tsk-arasu commited on
Commit
719d0fc
·
verified ·
1 Parent(s): 1dfe344

Upload folder using huggingface_hub

Browse files
REPORT.md ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Null Pointer Dereference in ExecuTorch Method::execute_instruction() via Missing Operator.overload (.pte)
2
+
3
+ **Target:** ExecuTorch 1.3.1 (.pte, huntr Model File Vulnerability program)
4
+ **Severity:** Low-Medium (Denial of Service) — CONDITIONAL on `EXECUTORCH_ENABLE_LOGGING=ON`
5
+ **CWE:** CWE-476 (NULL Pointer Dereference)
6
+ **Component:** `runtime/executor/method.cpp`
7
+ **Authentication Required:** No — requires a victim application to execute an attacker-supplied `.pte` file whose method contains an operator that legitimately fails during execution (a routine, expected occurrence).
8
+
9
+ ## Summary
10
+
11
+ `Method::execute_instruction()`'s `KernelCall` failure-logging path calls `op->overload()->c_str()` unconditionally when a kernel fails. `Operator.overload` is an optional flatbuffer field — a `.pte` file with an `Operator` entry that has a `name` but no `overload` triggers a null-pointer dereference the moment that operator's kernel returns any error during normal execution (e.g. a routine input-shape validation failure). This is distinct from all prior findings in this investigation: it is the first bug reached through actual kernel **execution** (`Method::execute()`), not just method loading, and its practical reachability is conditional on a specific (but officially documented and widely used) build configuration flag.
12
+
13
+ ## Vulnerability Details
14
+
15
+ `runtime/executor/method.cpp`'s `Method::execute_instruction()`:
16
+
17
+ ```cpp
18
+ case executorch_flatbuffer::InstructionArguments::KernelCall: {
19
+ ...
20
+ chain.kernels_[step_state_.instr_idx](context, args);
21
+ err = context.failure_state();
22
+ if (err != Error::Ok) {
23
+ auto op_index = instruction->instr_args_as_KernelCall()->op_index();
24
+ ET_UNUSED auto op = serialization_plan_->operators()->Get(op_index);
25
+ ET_LOG(
26
+ Error,
27
+ "KernelCall failed at instruction %" ET_PRIsize_t ":%" ET_PRIsize_t
28
+ " in operator %s.%s: 0x%x",
29
+ step_state_.chain_idx,
30
+ step_state_.instr_idx,
31
+ op->name()->c_str(),
32
+ op->overload()->c_str(), // <-- unguarded, crash site
33
+ (unsigned int)err);
34
+ ...
35
+ ```
36
+
37
+ The sibling function `populate_operator_name()` (also in `method.cpp`, used during `Method::init()`'s operator resolution) correctly guards this exact field:
38
+
39
+ ```cpp
40
+ const bool has_overload =
41
+ op->overload() != nullptr && op->overload()->size() > 0;
42
+ ...
43
+ has_overload ? op->overload()->c_str() : "",
44
+ ```
45
+
46
+ `execute_instruction()`'s error-logging path independently re-implements similar message construction but omits this guard.
47
+
48
+ ## An Important Investigative Finding: The Crash Is Masked By Default
49
+
50
+ During investigation, an initial test of this hypothesis against the default sanitized build configuration (`EXECUTORCH_ENABLE_LOGGING` unset, which defaults to OFF per `CMakeLists.txt`) did **not** crash. Root-cause analysis revealed why: `ET_LOG(...)` expands to `((void)0)` when `ET_LOG_ENABLED=0` — the entire log statement, including the `op->overload()->c_str()` argument expression, is never compiled into the code path at all in that configuration.
51
+
52
+ This was verified two ways before concluding the bug was real:
53
+
54
+ 1. A standalone minimal reproduction program (`check_null.cpp`, not part of the final PoC package) directly loaded the crafted `.pte`, confirmed `op->overload()` returns `nullptr`, and confirmed calling `->c_str()` on it produces the exact `member call on null pointer of type 'flatbuffers::String'` UBSan error.
55
+ 2. The full sanitized library was rebuilt with `-DEXECUTORCH_ENABLE_LOGGING=ON` — a real, non-default, but officially documented CMake option, explicitly set in numerous real upstream build scripts (the XNNPACK delegate tutorial, Vulkan delegate tutorial, Qualcomm backend `build.sh`, Cadence backend build scripts, Arm VGF tutorials, and the Llama example README all set `-DEXECUTORCH_ENABLE_LOGGING=ON`). Against this build, the crash reproduced reliably.
56
+
57
+ This report is filed with that caveat stated explicitly rather than either hiding it (which would overclaim universal reachability) or omitting the finding (which would underclaim, since `EXECUTORCH_ENABLE_LOGGING=ON` is a genuine, common, documented production configuration).
58
+
59
+ ## Steps to Reproduce
60
+
61
+ ### Environment
62
+ Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access.
63
+
64
+ ### 1. Build ExecuTorch with sanitizers AND logging enabled
65
+
66
+ ```bash
67
+ cmake -S executorch -B build \
68
+ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
69
+ -DCMAKE_C_COMPILER=clang-16 -DCMAKE_CXX_COMPILER=clang++-16 \
70
+ -DEXECUTORCH_ENABLE_PROGRAM_VERIFICATION=ON \
71
+ -DEXECUTORCH_ENABLE_LOGGING=ON \
72
+ -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
73
+ -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all" \
74
+ -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \
75
+ -DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON \
76
+ -DEXECUTORCH_BUILD_TESTS=OFF -DEXECUTORCH_BUILD_PYBIND=OFF -DEXECUTORCH_BUILD_XNNPACK=OFF \
77
+ -G Ninja
78
+ ninja -C build executorch_core extension_flat_tensor extension_data_loader
79
+ ```
80
+
81
+ ### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, included in this report)
82
+
83
+ This harness registers fake, deliberately-failing kernels (including one named `test_op` with no overload suffix, and a fake backend) and calls `Program::load_method()` + `Method::execute()` — exercising real kernel invocation, not just metadata:
84
+
85
+ ```bash
86
+ export ET_PARENT=/path/to/parent-of-executorch
87
+ C10_INC="$ET_SRC/runtime/core/portable_type/c10"
88
+ 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"
89
+
90
+ clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
91
+ $INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
92
+ -c poc/harness_execute_fuzzer.cpp -o harness.o
93
+
94
+ clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
95
+ "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
96
+ "$ET_BUILD/libexecutorch_core.a"
97
+ ```
98
+
99
+ ### 3. PoC file
100
+
101
+ `poc/poc_operator_overload_null.pte` (368 bytes, **included in this report — sha256 `f6b855b9d4bb69df0519e9954627703d88b9ef4da7104bbd52a6ba33239c3919`**) is a hand-crafted `.pte` containing:
102
+ - One `Operator` entry named `test_op` with **no** `overload` field
103
+ - One `KernelCall` instruction referencing that operator
104
+ - A registered (in the harness) always-failing kernel named `test_op` that matches this operator without needing an overload suffix
105
+
106
+ ### 4. Trigger the crash
107
+
108
+ ```bash
109
+ export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
110
+ export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
111
+ ./poc_harness -timeout=5 -runs=0 poc/poc_operator_overload_null.pte
112
+ ```
113
+
114
+ ### Expected result (secure behavior)
115
+ `Method::execute()` should return the kernel's error cleanly (and, if logging, produce a valid log message omitting the missing overload) rather than crashing while constructing the log message.
116
+
117
+ ### Actual result — verified against the pristine, unmodified ExecuTorch 1.3.1 source (with `EXECUTORCH_ENABLE_LOGGING=ON`)
118
+
119
+ ```
120
+ Running: poc/poc_operator_overload_null.pte
121
+ runtime/executor/method.cpp:1487:9: runtime error: member call on null pointer of type 'flatbuffers::String'
122
+ SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:1487:9 in
123
+ ==<pid>== ERROR: libFuzzer: deadly signal
124
+ #0 ... (abort machinery)
125
+ Method::execute_instruction (method.cpp:1487)
126
+ Method::execute (method.cpp:1758)
127
+ ```
128
+
129
+ **Reproduced 3/3 identical runs** (re-verified live for this report):
130
+ ```
131
+ run 1: member call on null pointer of type 'flatbuffers::String'
132
+ run 2: (identical)
133
+ run 3: (identical)
134
+ ```
135
+
136
+ Root cause was independently corroborated via a standalone minimal C++ program that loads the same `.pte`, confirms `op->overload()` is null, and reproduces the identical crash signature outside of the fuzzing harness entirely.
137
+
138
+ ## Impact
139
+
140
+ **Who is affected:** Any application executing a method whose kernel fails at runtime for an operator with no `overload` field — this is triggered by ordinary, expected kernel error handling (shape mismatches, unsupported dtypes, etc.), not an artificial condition — **provided the deployment was built with `EXECUTORCH_ENABLE_LOGGING=ON`**.
141
+
142
+ **What the attacker can do:** Cause a deterministic crash on operator failure, in builds with logging enabled.
143
+
144
+ **What's at risk:** Availability only, and only in the logging-enabled configuration.
145
+
146
+ **Why the conditional framing:** `EXECUTORCH_ENABLE_LOGGING` defaults to OFF in the CMake build (`ET_LOG_ENABLED=0`), which compiles the vulnerable call entirely out of the binary. However, this is not a safety net by design — it's an artifact of the logging macro's no-op expansion — and the flag is explicitly turned ON in numerous official ExecuTorch example/tutorial build scripts for XNNPACK, Vulkan, Qualcomm, Cadence, and Arm backends, plus the Llama example. Deployments following those documented instructions are affected.
147
+
148
+ ## Suggested Remediation
149
+
150
+ ```cpp
151
+ auto op_index = instruction->instr_args_as_KernelCall()->op_index();
152
+ ET_UNUSED auto op = serialization_plan_->operators()->Get(op_index);
153
+ const char* overload_str =
154
+ (op->overload() != nullptr) ? op->overload()->c_str() : "";
155
+ ET_LOG(
156
+ Error,
157
+ "KernelCall failed at instruction %" ET_PRIsize_t ":%" ET_PRIsize_t
158
+ " in operator %s.%s: 0x%x",
159
+ step_state_.chain_idx,
160
+ step_state_.instr_idx,
161
+ op->name()->c_str(),
162
+ overload_str,
163
+ (unsigned int)err);
164
+ ```
165
+
166
+ This mirrors the existing, correct pattern already used by `populate_operator_name()` for the same field.
167
+
168
+ **Design recommendation:** consider a small shared helper for extracting an operator's overload string safely, used by both `populate_operator_name()` and `execute_instruction()`'s failure-logging path, so this field's optionality doesn't need to be independently re-derived (and potentially missed) in future call sites.
169
+
170
+ A regression test should register a deliberately-failing kernel for an `Operator` entry with no `overload` field, build with `EXECUTORCH_ENABLE_LOGGING=ON`, and assert `Method::execute()` returns a clean `Error` rather than crashing during failure logging.
171
+
172
+ ## Files Included in This Report
173
+
174
+ - `poc/poc_operator_overload_null.pte` — the 368-byte PoC file (sha256 `f6b855b9d4bb69df0519e9954627703d88b9ef4da7104bbd52a6ba33239c3919`)
175
+ - `poc/harness_execute_fuzzer.cpp` — the harness used to trigger and reproduce the crash, including fake kernel/backend registration for exercising `Method::execute()`
176
+
177
+ ## huntr Submission Note
178
+
179
+ Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing. `poc/poc_operator_overload_null.pte` is ready for that upload. Given the conditional build-flag dependency, the submission should state this caveat explicitly per the analysis above.
poc/harness_execute_fuzzer.cpp ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // libFuzzer harness for Program::load_method() + Method::execute().
2
+ // Registers a fake kernel (for every op name that might appear) that ALWAYS
3
+ // calls context.fail(), to deterministically exercise the KernelCall failure
4
+ // logging path in Method::execute_instruction() - specifically testing
5
+ // whether op->overload()->c_str() is dereferenced unconditionally when
6
+ // overload is null (optional per schema).
7
+ //
8
+ // Also registers a fake backend (matching common backend id strings) whose
9
+ // init()/execute() are minimal, to exercise BackendDelegate::Init() and
10
+ // GetProcessedData() paths without requiring a real backend implementation.
11
+ #include <cstddef>
12
+ #include <cstdint>
13
+ #include <vector>
14
+
15
+ #include <executorch/extension/data_loader/buffer_data_loader.h>
16
+ #include <executorch/runtime/backend/interface.h>
17
+ #include <executorch/runtime/executor/method.h>
18
+ #include <executorch/runtime/executor/program.h>
19
+ #include <executorch/runtime/core/memory_allocator.h>
20
+ #include <executorch/runtime/core/hierarchical_allocator.h>
21
+ #include <executorch/runtime/executor/memory_manager.h>
22
+ #include <executorch/runtime/kernel/operator_registry.h>
23
+ #include <executorch/runtime/kernel/kernel_runtime_context.h>
24
+ #include <executorch/runtime/platform/runtime.h>
25
+
26
+ using executorch::extension::BufferDataLoader;
27
+ using executorch::runtime::Program;
28
+ using executorch::runtime::Method;
29
+ using executorch::runtime::MemoryAllocator;
30
+ using executorch::runtime::HierarchicalAllocator;
31
+ using executorch::runtime::MemoryManager;
32
+ using executorch::runtime::Span;
33
+ using executorch::runtime::EValue;
34
+ using executorch::runtime::Error;
35
+ using executorch::runtime::Result;
36
+ using executorch::runtime::KernelRuntimeContext;
37
+ using executorch::runtime::Kernel;
38
+ using executorch::runtime::register_kernels;
39
+ using executorch::runtime::BackendInterface;
40
+ using executorch::runtime::Backend;
41
+ using executorch::runtime::register_backend;
42
+ using executorch::runtime::DelegateHandle;
43
+ using executorch::runtime::FreeableBuffer;
44
+ using executorch::runtime::ArrayRef;
45
+ using executorch::runtime::CompileSpec;
46
+ using executorch::runtime::BackendInitContext;
47
+ using executorch::runtime::BackendExecutionContext;
48
+
49
+ // A kernel that always fails - forces Method::execute_instruction()'s
50
+ // KernelCall failure-logging path (which dereferences op->overload()) on
51
+ // EVERY KernelCall instruction, regardless of which operator is named.
52
+ void always_fail_kernel(KernelRuntimeContext& context, Span<EValue*> /*args*/) {
53
+ context.fail(Error::Internal);
54
+ }
55
+
56
+ // A minimal backend that succeeds trivially, to exercise
57
+ // BackendDelegate::Init()/GetProcessedData() without needing a real backend.
58
+ class FuzzTestBackend final : public BackendInterface {
59
+ public:
60
+ bool is_available() const override {
61
+ return true;
62
+ }
63
+ Result<DelegateHandle*> init(
64
+ BackendInitContext& /*context*/,
65
+ FreeableBuffer* /*processed*/,
66
+ ArrayRef<CompileSpec> /*compile_specs*/) const override {
67
+ return static_cast<DelegateHandle*>(nullptr);
68
+ }
69
+ Error execute(
70
+ BackendExecutionContext& /*context*/,
71
+ DelegateHandle* /*handle*/,
72
+ Span<EValue*> /*args*/) const override {
73
+ return Error::Ok;
74
+ }
75
+ };
76
+
77
+ static bool g_initialized = false;
78
+ static FuzzTestBackend* g_backend = nullptr;
79
+
80
+ extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
81
+ if (!g_initialized) {
82
+ executorch::runtime::runtime_init();
83
+
84
+ // Register a wildcard-ish set of fallback kernels for common op names
85
+ // seen in the seed corpus, all pointing to always_fail_kernel. We can't
86
+ // truly wildcard-match in this registry, so register kernels for the
87
+ // most common ops that appear in the existing .pte seed corpus.
88
+ static const Kernel kernels[] = {
89
+ Kernel("aten::add.out", always_fail_kernel),
90
+ Kernel("aten::add.Tensor_out", always_fail_kernel),
91
+ Kernel("aten::mul.out", always_fail_kernel),
92
+ Kernel("aten::mul.Tensor_out", always_fail_kernel),
93
+ Kernel("aten::sub.out", always_fail_kernel),
94
+ Kernel("aten::relu.out", always_fail_kernel),
95
+ Kernel("aten::linear.out", always_fail_kernel),
96
+ Kernel("aten::cat.out", always_fail_kernel),
97
+ Kernel("aten::view_copy.out", always_fail_kernel),
98
+ Kernel("aten::index.Tensor_out", always_fail_kernel),
99
+ Kernel("aten::_softmax.out", always_fail_kernel),
100
+ Kernel("aten::convolution.out", always_fail_kernel),
101
+ Kernel("aten::permute_copy.out", always_fail_kernel),
102
+ Kernel("aten::_to_copy.out", always_fail_kernel),
103
+ // No-overload variants: matches Operator entries whose `overload`
104
+ // field is null/absent, which is the specific condition needed to
105
+ // test whether Method::execute_instruction()'s KernelCall failure
106
+ // path dereferences op->overload() unconditionally.
107
+ Kernel("aten::add", always_fail_kernel),
108
+ Kernel("aten::mul", always_fail_kernel),
109
+ Kernel("aten::sub", always_fail_kernel),
110
+ Kernel("aten::relu", always_fail_kernel),
111
+ Kernel("aten::linear", always_fail_kernel),
112
+ Kernel("aten::cat", always_fail_kernel),
113
+ Kernel("test_op", always_fail_kernel),
114
+ Kernel("test_op_no_overload", always_fail_kernel),
115
+ };
116
+ register_kernels({kernels, sizeof(kernels) / sizeof(kernels[0])});
117
+
118
+ static FuzzTestBackend backend_instance;
119
+ g_backend = &backend_instance;
120
+ static const char* backend_names[] = {
121
+ "XnnpackBackend", "CoreMLBackend", "QnnBackend",
122
+ "VulkanBackend", "MPSBackend", "TCE0", "TCE1"};
123
+ for (const char* name : backend_names) {
124
+ register_backend(Backend{name, g_backend});
125
+ }
126
+
127
+ g_initialized = true;
128
+ }
129
+
130
+ constexpr std::size_t kMaxInput = 8U * 1024U * 1024U;
131
+ if (data == nullptr || size == 0 || size > kMaxInput) {
132
+ return 0;
133
+ }
134
+
135
+ BufferDataLoader loader(data, size);
136
+ auto program = Program::load(&loader, Program::Verification::InternalConsistency);
137
+ if (!program.ok()) {
138
+ return 0;
139
+ }
140
+
141
+ static std::vector<uint8_t> method_pool(512 * 1024);
142
+ static std::vector<uint8_t> planned_pool(256 * 1024);
143
+ MemoryAllocator method_allocator(method_pool.size(), method_pool.data());
144
+ static Span<uint8_t> planned_span(planned_pool.data(), planned_pool.size());
145
+ HierarchicalAllocator planned_memory({&planned_span, 1});
146
+ MemoryManager mm(&method_allocator, &planned_memory, nullptr);
147
+
148
+ auto& p = program.get();
149
+ auto n = p.num_methods();
150
+ for (size_t i = 0; i < n; ++i) {
151
+ auto name = p.get_method_name(i);
152
+ if (!name.ok()) continue;
153
+ auto method = p.load_method(name.get(), &mm);
154
+ if (!method.ok()) {
155
+ #ifdef ET_FUZZ_DEBUG
156
+ fprintf(stderr, "[dbg] load_method failed: 0x%x\n",
157
+ static_cast<unsigned int>(method.error()));
158
+ #endif
159
+ continue;
160
+ }
161
+ // Attempt to set trivial inputs where possible, then execute. We don't
162
+ // try hard to satisfy every possible input shape - we're hunting for
163
+ // crashes during initialization and the KernelCall failure path, not
164
+ // testing correct numerical execution.
165
+ auto& m = method.get();
166
+ auto exec_err = m.execute();
167
+ #ifdef ET_FUZZ_DEBUG
168
+ fprintf(stderr, "[dbg] execute() returned: 0x%x\n",
169
+ static_cast<unsigned int>(exec_err));
170
+ #endif
171
+ (void)exec_err;
172
+ }
173
+ return 0;
174
+ }
poc/poc_operator_overload_null.pte ADDED
Binary file (368 Bytes). View file