tsk-arasu commited on
Commit
1ec1150
·
verified ·
1 Parent(s): 2ac4a11

Upload folder using huggingface_hub

Browse files
REPORT.md ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Null Pointer Dereference in ExecuTorch Program::get_backend_delegate_data() via Missing Program.backend_delegate_data (.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/program.cpp`
7
+ **Authentication Required:** No — requires a victim application to load a `.pte` file whose method has a backend delegate referencing INLINE-located processed data, with at least one backend registered/available (a routine deployment configuration).
8
+
9
+ ## Summary
10
+
11
+ `Program::get_backend_delegate_data()` dereferences `Program.backend_delegate_data()` (an optional top-level flatbuffer field) directly inside its bounds-check condition, with no null guard. A `.pte` file with an `ExecutionPlan.delegates` entry whose `processed` field specifies `DataLocation::INLINE`, while the `Program`'s top-level `backend_delegate_data` list is entirely absent, crashes the moment `Method::init()` attempts to resolve that delegate — before any execution begins. Unlike a sibling finding in this same investigation (the `Operator.overload` null-deref), this crash sits directly in the bounds-check condition itself, not merely inside a log-message argument — making it **unconditionally reachable regardless of build configuration** (no dependency on `EXECUTORCH_ENABLE_LOGGING`).
12
+
13
+ This bug was confirmed **twice independently**: once against this session's incrementally patched build, and once against a **freshly rebuilt, wholly unmodified pristine 1.3.1 source tree**, ruling out any possibility of a local-patch artifact.
14
+
15
+ ## Vulnerability Details
16
+
17
+ `runtime/executor/program.cpp`'s `Program::get_backend_delegate_data()`:
18
+
19
+ ```cpp
20
+ Error Program::get_backend_delegate_data(
21
+ size_t index,
22
+ const void** out_data,
23
+ size_t* out_size) const {
24
+ const auto* data_list =
25
+ static_cast<const executorch_flatbuffer::Program*>(internal_program_)
26
+ ->backend_delegate_data();
27
+ ET_CHECK_OR_RETURN_ERROR(
28
+ index < data_list->size(), // <-- crash site: data_list may be null
29
+ NotFound,
30
+ "index %zu >= list size %" PRIu32,
31
+ index,
32
+ data_list->size());
33
+ ...
34
+ ```
35
+
36
+ `Program.backend_delegate_data` is declared as an optional `[BackendDelegateInlineData]` list in `schema/program.fbs`:
37
+
38
+ ```
39
+ // List of delegate data. Pointed to by BackendDelegateDataReference.
40
+ backend_delegate_data: [BackendDelegateInlineData];
41
+ ```
42
+
43
+ This function is called from `BackendDelegate::GetProcessedData()` (`method.cpp`) whenever a delegate's `processed` field specifies `DataLocation::INLINE`:
44
+
45
+ ```cpp
46
+ case executorch_flatbuffer::DataLocation::INLINE: {
47
+ const void* data;
48
+ size_t size;
49
+ Error err = program->get_backend_delegate_data(
50
+ processed->index(), &data, &size);
51
+ ...
52
+ ```
53
+
54
+ Which in turn is called from `BackendDelegate::Init()` during `Method::init()`'s delegate-resolution loop — reached during ordinary method **loading**, for any delegate whose registered backend is available (`get_backend_class(id) != nullptr && backend->is_available()`), a standard, expected condition for any deployment that has compiled in and registered at least one hardware-acceleration backend (which most real ExecuTorch deployments do).
55
+
56
+ ## Steps to Reproduce
57
+
58
+ ### Environment
59
+ Linux x86-64, ExecuTorch 1.3.1 pristine source, clang-16, CMake, Ninja. No authentication, no host access.
60
+
61
+ ### 1. Build ExecuTorch with sanitizers (logging enabled or disabled — this bug reproduces either way)
62
+
63
+ Same build as REPORT-11 Step 1 (or the simpler REPORT-01 build — `EXECUTORCH_ENABLE_LOGGING` does not affect this finding).
64
+
65
+ ### 2. Build the execute-level harness (`poc/harness_execute_fuzzer.cpp`, same harness as REPORT-11, included in this report)
66
+
67
+ This harness registers fake backends under common names (`XnnpackBackend`, `QnnBackend`, `CoreMLBackend`, etc.) that trivially succeed on `init()`, letting us exercise the real `BackendDelegate::Init()` code path:
68
+
69
+ ```bash
70
+ export ET_PARENT=/path/to/parent-of-executorch
71
+ C10_INC="$ET_SRC/runtime/core/portable_type/c10"
72
+ 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"
73
+
74
+ clang++-16 -std=c++17 -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all \
75
+ $INCLUDES -DFLATBUFFERS_MAX_ALIGNMENT=1024 -DC10_USING_CUSTOM_GENERATED_MACROS \
76
+ -c poc/harness_execute_fuzzer.cpp -o harness.o
77
+
78
+ clang++-16 -fsanitize=fuzzer,address,undefined -o poc_harness harness.o \
79
+ "$ET_BUILD/extension/data_loader/libextension_data_loader.a" \
80
+ "$ET_BUILD/libexecutorch_core.a"
81
+ ```
82
+
83
+ ### 3. PoC file
84
+
85
+ `poc/poc_backend_delegate_data_null.pte` (304 bytes, **included in this report — sha256 `2f3be1201b1a426b19d44d2b0886ba12ed872ec4e3f4b02d65457b6c4f441165`**) contains:
86
+ - One `BackendDelegate` entry with `id = "XnnpackBackend"` and `processed = { location: INLINE, index: 0 }`
87
+ - **No** `backend_delegate_data` field at the `Program` level at all
88
+
89
+ ### 4. Trigger the crash
90
+
91
+ ```bash
92
+ export ASAN_OPTIONS="abort_on_error=1:symbolize=0"
93
+ export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=0"
94
+ ./poc_harness -timeout=5 -runs=0 poc/poc_backend_delegate_data_null.pte
95
+ ```
96
+
97
+ ### Expected result (secure behavior)
98
+ `Method::init()` should return `Error::NotFound` (or similar) cleanly when a delegate references INLINE data but the Program has no `backend_delegate_data` list, rather than crashing while checking bounds.
99
+
100
+ ### Actual result — verified against BOTH a patched build AND an independently rebuilt, wholly unmodified pristine ExecuTorch 1.3.1 source tree
101
+
102
+ ```
103
+ Running: poc/poc_backend_delegate_data_null.pte
104
+ runtime/executor/program.cpp:541:3: runtime error: member call on null pointer of type 'flatbuffers::Vector<flatbuffers::Offset<executorch_flatbuffer::BackendDelegateInlineData>>'
105
+ SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior runtime/executor/program.cpp:541:3 in
106
+ ==<pid>== ERROR: libFuzzer: deadly signal
107
+ #0 ... (abort machinery)
108
+ Program::get_backend_delegate_data (program.cpp:541)
109
+ BackendDelegate::GetProcessedData (method.cpp:202)
110
+ BackendDelegate::Init (method.cpp:97)
111
+ Method::init (method.cpp:971)
112
+ Method::load (method.cpp:862)
113
+ ```
114
+
115
+ **Reproduced 3/3 identical runs against the independently rebuilt pristine source** (re-verified live for this report, using a fresh build with zero local modifications):
116
+ ```
117
+ run 1: member call on null pointer of type 'flatbuffers::Vector<...>'
118
+ run 2: (identical)
119
+ run 3: (identical)
120
+ ```
121
+
122
+ ## Impact
123
+
124
+ **Who is affected:** Any application calling `Program::load_method()` on a method whose delegates reference INLINE processed data, where the Program's `backend_delegate_data` list is absent — this is triggered during **loading**, not execution, and requires only that the victim has registered a backend under a name the attacker can predict (most deployments register at least one real hardware-acceleration backend).
125
+
126
+ **What the attacker can do:** Cause a deterministic crash during method loading, unconditionally (no build-flag dependency).
127
+
128
+ **What's at risk:** Availability only.
129
+
130
+ **Why not Critical:** Controlled null-pointer dereference, no memory corruption or code execution demonstrated.
131
+
132
+ ## Suggested Remediation
133
+
134
+ ```cpp
135
+ Error Program::get_backend_delegate_data(
136
+ size_t index,
137
+ const void** out_data,
138
+ size_t* out_size) const {
139
+ const auto* data_list =
140
+ static_cast<const executorch_flatbuffer::Program*>(internal_program_)
141
+ ->backend_delegate_data();
142
+ if (data_list == nullptr) {
143
+ ET_LOG(Error, "Program has no backend_delegate_data list");
144
+ return Error::NotFound;
145
+ }
146
+ ET_CHECK_OR_RETURN_ERROR(
147
+ index < data_list->size(),
148
+ NotFound,
149
+ "index %zu >= list size %" PRIu32,
150
+ index,
151
+ data_list->size());
152
+ ...
153
+ ```
154
+
155
+ **Design recommendation:** consider validating at `Program::load()` time (in `validate_program()`, `runtime/executor/program_validation.cpp`) that if any `ExecutionPlan.delegates[].processed.location == INLINE` exists anywhere in the program, `Program.backend_delegate_data` must also be present — catching this cross-field consistency issue once, at load time, rather than at each individual accessor.
156
+
157
+ A regression test should build a `.pte` with one `BackendDelegate` referencing INLINE processed data at index 0, and no `backend_delegate_data` field on the `Program`, asserting `Method::load()` returns a clean `Error::NotFound` rather than crashing.
158
+
159
+ ## Files Included in This Report
160
+
161
+ - `poc/poc_backend_delegate_data_null.pte` — the 304-byte PoC file (sha256 `2f3be1201b1a426b19d44d2b0886ba12ed872ec4e3f4b02d65457b6c4f441165`)
162
+ - `poc/harness_execute_fuzzer.cpp` — the harness used to trigger and reproduce the crash, including fake backend registration for exercising `BackendDelegate::Init()`
163
+
164
+ ## huntr Submission Note
165
+
166
+ Per the huntr MFV program's submission requirements, this PoC needs to be uploaded to a public HuggingFace repository before filing. `poc/poc_backend_delegate_data_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_backend_delegate_data_null.pte ADDED
Binary file (304 Bytes). View file