Skip to content

Commit fb8307e

Browse files
Validate and lock SharedBuffer allocations (pytorch#24463)
Summary: ## TL;DR Make the QNN `SharedBuffer` manager safe for concurrent callers. Reject invalid `AllocMem` requests before they reach the RPC allocator. ## Problem `SharedBuffer` is a process-wide singleton. Its four maps (aligned-to-original pointer, allocated size, and the tensor-to-custom-memory map and its reverse) had no synchronization. Applications call `QnnExecuTorchAllocCustomMem`, `QnnExecuTorchAddCustomMemTensorAddr` and `QnnExecuTorchFreeCustomMem` while `QnnManager` reads the same maps to register tensor memory. Allocate, register, lookup and free on different threads could therefore race. `AllocMem` also had three validation and bookkeeping bugs: - It accepted a zero size, a zero or non-power-of-two alignment, and sizes whose alignment padding overflows the `int32_t` rpcmem size. - It recorded an allocated size before confirming the pointer insertion. - When that insertion failed, it freed the allocation but still returned the aligned pointer. ## Solution A per-instance `memory_mutex_` now guards every map access. Before calling the RPC allocator, `AllocMem` rejects a zero size, a zero or non-power-of-two alignment, and any request where `bytes + alignment` exceeds `INT32_MAX`. A new allocation stays in a scoped owner until its bookkeeping is installed. If installing the bookkeeping fails after the RPC allocation succeeds, the scoped owner frees the original rpcmem pointer, removes any partial entries, and `AllocMem` returns null. A new `shared_buffer_test` drives the manager through fake RPC callbacks, so it needs no device. When the standard library supports throwing allocation failures, `AllocMem` now catches `std::bad_alloc` from either bookkeeping map insertion and returns `nullptr` after the scoped owner rolls back the allocation. The recovery test forces each insertion to fail and verifies that the RPC buffer is freed, both maps are empty, and allocation succeeds afterward. Builds without exception support retain their existing allocation-failure behavior. Authored with Claude Code and Codex. Differential Revision: D128567553
1 parent c54b0ed commit fb8307e

5 files changed

Lines changed: 300 additions & 10 deletions

File tree

‎backends/qualcomm/runtime/SharedBuffer.cpp‎

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77
*/
88
#include <executorch/backends/qualcomm/runtime/Logging.h>
99
#include <executorch/backends/qualcomm/runtime/SharedBuffer.h>
10+
#include <executorch/runtime/platform/compiler.h>
1011

1112
#include <pal/DynamicLoading.h>
1213

14+
#include <limits>
15+
#include <new>
16+
1317
// Refer to the QNN HTP Shared Buffer Tutorial
1418
// in Qualcomm® AI Engine Direct document
1519
constexpr uint8_t RPCMEM_HEAP_ID_SYSTEM = 25;
@@ -63,6 +67,7 @@ intptr_t alignTo(size_t alignment, intptr_t offset) {
6367
std::mutex SharedBuffer::init_mutex_;
6468

6569
void* SharedBuffer::GetCustomMemBase(void* buf) {
70+
std::lock_guard<std::mutex> lock(memory_mutex_);
6671
auto it = tensor_addr_to_custom_mem_.find(buf);
6772
if (it == tensor_addr_to_custom_mem_.end()) {
6873
return nullptr;
@@ -71,6 +76,7 @@ void* SharedBuffer::GetCustomMemBase(void* buf) {
7176
}
7277

7378
size_t SharedBuffer::GetAllocatedSize(void* buf) {
79+
std::lock_guard<std::mutex> lock(memory_mutex_);
7480
auto it = allocated_size_map_.find(buf);
7581
if (it == allocated_size_map_.end()) {
7682
return 0;
@@ -107,26 +113,56 @@ void* SharedBuffer::AllocMem(size_t bytes, size_t alignment) {
107113
QNN_EXECUTORCH_LOG_ERROR("Shared memory not initialized.");
108114
return nullptr;
109115
}
116+
const size_t max_bytes = std::numeric_limits<int32_t>::max();
117+
if (bytes == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0 ||
118+
alignment > max_bytes || bytes > max_bytes - alignment) {
119+
QNN_EXECUTORCH_LOG_ERROR("Invalid shared memory size or alignment.");
120+
return nullptr;
121+
}
122+
std::lock_guard<std::mutex> lock(memory_mutex_);
110123
// do alignment:
111124
auto allocate_bytes = static_cast<int32_t>(bytes + alignment);
112-
void* buf = rpc_mem_alloc_(
113-
RPCMEM_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, allocate_bytes);
114-
if (buf == nullptr) {
125+
void* aligned_buf = nullptr;
126+
bool restored = false;
127+
auto release_allocation = [&](void* original) {
128+
if (restored) {
129+
restore_map_.erase(aligned_buf);
130+
allocated_size_map_.erase(aligned_buf);
131+
}
132+
rpc_mem_free_(original);
133+
};
134+
std::unique_ptr<void, decltype(release_allocation)> allocation(
135+
rpc_mem_alloc_(
136+
RPCMEM_HEAP_ID_SYSTEM, RPCMEM_DEFAULT_FLAGS, allocate_bytes),
137+
release_allocation);
138+
if (!allocation) {
115139
QNN_EXECUTORCH_LOG_WARN("Failed to allocate the tensor by RPC memory.");
116140
return nullptr;
117141
}
118-
auto aligned_buf = reinterpret_cast<void*>(
119-
alignTo(alignment, reinterpret_cast<intptr_t>(buf)));
120-
bool status = restore_map_.insert({aligned_buf, buf}).second;
121-
allocated_size_map_.insert({aligned_buf, allocate_bytes});
122-
if (!status) {
123-
QNN_EXECUTORCH_LOG_ERROR("Failed to allocate the tensor by RPC memory.");
124-
rpc_mem_free_(buf);
142+
// NOLINTNEXTLINE(performance-no-int-to-ptr)
143+
aligned_buf = reinterpret_cast<void*>(
144+
alignTo(alignment, reinterpret_cast<intptr_t>(allocation.get())));
145+
#if ET_HAS_EXCEPTIONS
146+
try {
147+
#endif
148+
restored = restore_map_.insert({aligned_buf, allocation.get()}).second;
149+
if (!restored) {
150+
QNN_EXECUTORCH_LOG_ERROR("Failed to allocate the tensor by RPC memory.");
151+
return nullptr;
152+
}
153+
allocated_size_map_.insert({aligned_buf, allocate_bytes});
154+
#if ET_HAS_EXCEPTIONS
155+
} catch (const std::bad_alloc&) {
156+
QNN_EXECUTORCH_LOG_ERROR("Failed to allocate shared memory bookkeeping.");
157+
return nullptr;
125158
}
159+
#endif
160+
restore_map_[aligned_buf] = allocation.release();
126161
return aligned_buf;
127162
}
128163

129164
int32_t SharedBuffer::MemToFd(void* buf) {
165+
std::lock_guard<std::mutex> lock(memory_mutex_);
130166
int32_t memFd = -1;
131167
if (!initialize_) {
132168
QNN_EXECUTORCH_LOG_ERROR("Shared memory not initialized.");
@@ -137,6 +173,7 @@ int32_t SharedBuffer::MemToFd(void* buf) {
137173
}
138174

139175
void SharedBuffer::FreeMem(void* buf) {
176+
std::lock_guard<std::mutex> lock(memory_mutex_);
140177
if (!initialize_) {
141178
QNN_EXECUTORCH_LOG_ERROR("Shared memory not initialized.");
142179
} else if (restore_map_.count(buf) == 0) {
@@ -157,6 +194,7 @@ void SharedBuffer::FreeMem(void* buf) {
157194
}
158195

159196
bool SharedBuffer::IsAllocated(void* buf) {
197+
std::lock_guard<std::mutex> lock(memory_mutex_);
160198
return restore_map_.count(buf) != 0U;
161199
}
162200

@@ -190,6 +228,7 @@ Error SharedBuffer::Load() {
190228
}
191229

192230
void SharedBuffer::AddCusomMemTensorAddr(void* tensor_addr, void* custom_mem) {
231+
std::lock_guard<std::mutex> lock(memory_mutex_);
193232
bool status =
194233
tensor_addr_to_custom_mem_.insert({tensor_addr, custom_mem}).second;
195234
if (!status) {

‎backends/qualcomm/runtime/SharedBuffer.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class SharedBuffer final {
6464
void* GetCustomMemBase(void* buf);
6565

6666
private:
67+
friend class SharedBufferTest;
6768
SharedBuffer() = default;
6869

6970
// dlopen RPCMem library and dlysm required functions
@@ -88,6 +89,7 @@ class SharedBuffer final {
8889
// remain linked to this custom memory.
8990
std::unordered_map<void*, std::unordered_set<void*>>
9091
custom_mem_to_tensor_addr_;
92+
std::mutex memory_mutex_;
9193
std::atomic_bool initialize_{false};
9294
static std::mutex init_mutex_;
9395
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target", "non_fbcode_target")
2+
load(":targets.bzl", "define_common_targets")
3+
4+
oncall("executorch")
5+
6+
non_fbcode_target(_kind = define_common_targets)
7+
fbcode_target(_kind = define_common_targets)
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/qualcomm/runtime/SharedBuffer.h>
10+
#include <executorch/runtime/platform/compiler.h>
11+
#include <gtest/gtest.h>
12+
#include <atomic>
13+
#include <cstdint>
14+
#include <cstdlib>
15+
#include <limits>
16+
#include <memory>
17+
#include <new>
18+
#include <thread>
19+
#include <vector>
20+
21+
#if ET_HAS_EXCEPTIONS && !defined(_GLIBCXX_RESTRICT_EXCEPTIONS)
22+
namespace {
23+
thread_local int allocationsUntilFailure = -1;
24+
} // namespace
25+
26+
void* operator new(std::size_t size) {
27+
if (allocationsUntilFailure >= 0 && allocationsUntilFailure-- == 0) {
28+
throw std::bad_alloc();
29+
}
30+
if (void* pointer = std::malloc(size == 0 ? 1 : size)) {
31+
return pointer;
32+
}
33+
throw std::bad_alloc();
34+
}
35+
36+
void operator delete(void* pointer) noexcept {
37+
std::free(pointer);
38+
}
39+
40+
void operator delete(void* pointer, std::size_t) noexcept {
41+
std::free(pointer);
42+
}
43+
44+
void* operator new(std::size_t size, const std::nothrow_t&) noexcept {
45+
try {
46+
return ::operator new(size);
47+
} catch (const std::bad_alloc&) {
48+
return nullptr;
49+
}
50+
}
51+
52+
void operator delete(void* pointer, const std::nothrow_t&) noexcept {
53+
std::free(pointer);
54+
}
55+
#endif
56+
57+
namespace executorch::backends::qnn {
58+
59+
class SharedBufferTest : public ::testing::Test {
60+
protected:
61+
void SetUp() override {
62+
allocationCalls_ = 0;
63+
buffer_.reset(new SharedBuffer());
64+
buffer_->rpc_mem_alloc_ = [](int, uint32_t, int bytes) -> void* {
65+
++allocationCalls_;
66+
return bytes > 0 && bytes <= 1024 * 1024
67+
? std::malloc(static_cast<size_t>(bytes))
68+
: nullptr;
69+
};
70+
buffer_->rpc_mem_free_ = [](void* pointer) { std::free(pointer); };
71+
buffer_->rpc_mem_to_fd_ = [](void*) { return 17; };
72+
buffer_->SetInitialize(true);
73+
}
74+
75+
void TearDown() override {
76+
for (const auto& allocation : buffer_->restore_map_) {
77+
std::free(allocation.second);
78+
}
79+
buffer_->SetInitialize(false);
80+
}
81+
82+
void useOffsetRpcAllocator() {
83+
buffer_->rpc_mem_alloc_ = [](int, uint32_t, int bytes) -> void* {
84+
rpcAllocation_ =
85+
static_cast<uint8_t*>(std::malloc(static_cast<size_t>(bytes) + 1)) +
86+
1;
87+
return rpcAllocation_;
88+
};
89+
buffer_->rpc_mem_free_ = [](void* pointer) {
90+
rpcFreed_ = pointer;
91+
std::free(static_cast<uint8_t*>(pointer) - 1);
92+
};
93+
}
94+
95+
bool empty() const {
96+
return buffer_->restore_map_.empty() &&
97+
buffer_->allocated_size_map_.empty() &&
98+
buffer_->tensor_addr_to_custom_mem_.empty() &&
99+
buffer_->custom_mem_to_tensor_addr_.empty();
100+
}
101+
102+
#if ET_HAS_EXCEPTIONS && !defined(_GLIBCXX_RESTRICT_EXCEPTIONS)
103+
void failBookkeepingAllocation(int successfulAllocations) {
104+
buffer_->restore_map_.reserve(1);
105+
buffer_->allocated_size_map_.reserve(1);
106+
buffer_->rpc_mem_alloc_ = [](int, uint32_t, int bytes) -> void* {
107+
rpcAllocation_ = std::malloc(static_cast<size_t>(bytes));
108+
return rpcAllocation_;
109+
};
110+
buffer_->rpc_mem_free_ = [](void* pointer) {
111+
rpcFreed_ = pointer;
112+
std::free(pointer);
113+
};
114+
rpcFreed_ = nullptr;
115+
allocationsUntilFailure = successfulAllocations;
116+
}
117+
#endif
118+
119+
std::unique_ptr<SharedBuffer> buffer_;
120+
inline static std::atomic<size_t> allocationCalls_{0};
121+
inline static void* rpcAllocation_{nullptr};
122+
inline static void* rpcFreed_{nullptr};
123+
};
124+
125+
TEST_F(SharedBufferTest, AlignsOffsetRpcAllocationAndFreesTheOriginal) {
126+
constexpr size_t kAlignment = 64;
127+
constexpr size_t kBytes = 128;
128+
useOffsetRpcAllocator();
129+
130+
auto* aligned = static_cast<uint8_t*>(buffer_->AllocMem(kBytes, kAlignment));
131+
ASSERT_NE(aligned, nullptr);
132+
auto* original = static_cast<uint8_t*>(rpcAllocation_);
133+
EXPECT_EQ(reinterpret_cast<uintptr_t>(aligned) % kAlignment, 0);
134+
EXPECT_GT(aligned, original);
135+
EXPECT_LE(aligned + kBytes, original + buffer_->GetAllocatedSize(aligned));
136+
buffer_->FreeMem(aligned);
137+
138+
EXPECT_EQ(rpcFreed_, original);
139+
EXPECT_TRUE(empty());
140+
}
141+
142+
TEST_F(SharedBufferTest, InvalidSizeAndAlignmentDoNotCallRpcAllocator) {
143+
EXPECT_EQ(buffer_->AllocMem(0, alignof(void*)), nullptr);
144+
EXPECT_EQ(buffer_->AllocMem(64, 0), nullptr);
145+
EXPECT_EQ(buffer_->AllocMem(64, 3), nullptr);
146+
EXPECT_EQ(
147+
buffer_->AllocMem(std::numeric_limits<int32_t>::max(), alignof(void*)),
148+
nullptr);
149+
EXPECT_EQ(
150+
buffer_->AllocMem(std::numeric_limits<size_t>::max(), alignof(void*)),
151+
nullptr);
152+
EXPECT_EQ(allocationCalls_.load(), 0);
153+
EXPECT_TRUE(empty());
154+
}
155+
156+
TEST_F(SharedBufferTest, ConcurrentAllocateRegisterLookupAndFree) {
157+
constexpr size_t kThreads = 4;
158+
constexpr size_t kAllocations = 256;
159+
constexpr size_t kBytes = 64;
160+
std::vector<void*> buffers(kThreads * kAllocations, nullptr);
161+
std::vector<std::thread> workers;
162+
workers.reserve(kThreads);
163+
std::atomic<bool> start{false};
164+
for (size_t worker = 0; worker < kThreads; ++worker) {
165+
workers.emplace_back([&, worker] {
166+
while (!start.load(std::memory_order_acquire)) {
167+
std::this_thread::yield();
168+
}
169+
for (size_t index = worker; index < buffers.size(); index += kThreads) {
170+
void* pointer = buffer_->AllocMem(kBytes, alignof(void*));
171+
ASSERT_NE(pointer, nullptr);
172+
buffers[index] = pointer;
173+
void* alias = static_cast<uint8_t*>(pointer) + sizeof(uint64_t);
174+
buffer_->AddCusomMemTensorAddr(alias, pointer);
175+
EXPECT_TRUE(buffer_->IsAllocated(pointer));
176+
EXPECT_EQ(buffer_->GetCustomMemBase(alias), pointer);
177+
EXPECT_GE(buffer_->GetAllocatedSize(pointer), kBytes);
178+
EXPECT_EQ(buffer_->MemToFd(pointer), 17);
179+
}
180+
});
181+
}
182+
start.store(true, std::memory_order_release);
183+
for (auto& worker : workers) {
184+
worker.join();
185+
}
186+
EXPECT_EQ(allocationCalls_.load(), buffers.size());
187+
188+
workers.clear();
189+
for (size_t worker = 0; worker < kThreads; ++worker) {
190+
workers.emplace_back([&, worker] {
191+
for (size_t index = worker; index < buffers.size(); index += kThreads) {
192+
void* pointer = buffers[index];
193+
ASSERT_NE(pointer, nullptr);
194+
void* alias = static_cast<uint8_t*>(pointer) + sizeof(uint64_t);
195+
buffer_->FreeMem(pointer);
196+
EXPECT_FALSE(buffer_->IsAllocated(pointer));
197+
EXPECT_EQ(buffer_->GetAllocatedSize(pointer), 0);
198+
EXPECT_EQ(buffer_->GetCustomMemBase(alias), nullptr);
199+
}
200+
});
201+
}
202+
for (auto& worker : workers) {
203+
worker.join();
204+
}
205+
EXPECT_TRUE(empty());
206+
}
207+
208+
#if ET_HAS_EXCEPTIONS && !defined(_GLIBCXX_RESTRICT_EXCEPTIONS)
209+
TEST_F(SharedBufferTest, BookkeepingAllocationFailureRollsBackAndReturnsNull) {
210+
// Reserved buckets leave one node allocation per map insertion.
211+
for (int successfulAllocations : {0, 1}) {
212+
failBookkeepingAllocation(successfulAllocations);
213+
void* result = buffer_->AllocMem(64, alignof(void*));
214+
allocationsUntilFailure = -1;
215+
EXPECT_EQ(result, nullptr);
216+
EXPECT_EQ(rpcFreed_, rpcAllocation_);
217+
EXPECT_TRUE(empty());
218+
219+
void* retry = buffer_->AllocMem(64, alignof(void*));
220+
ASSERT_NE(retry, nullptr);
221+
buffer_->FreeMem(retry);
222+
EXPECT_TRUE(empty());
223+
}
224+
}
225+
#endif
226+
227+
} // namespace executorch::backends::qnn
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("@fbsource//tools/build_defs:default_platform_defs.bzl", "CXX")
2+
load("@fbsource//xplat/executorch/backends/qualcomm/third-party:third_party_libs.bzl", "qnn_third_party_dep")
3+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
4+
5+
def define_common_targets():
6+
runtime.cxx_test(
7+
name = "shared_buffer_test",
8+
srcs = ["SharedBufferTest.cpp"],
9+
compatible_with = [] if runtime.is_oss else ["ovr_config//os:linux"],
10+
platforms = [CXX],
11+
deps = [
12+
"//executorch/backends/qualcomm/runtime:runtime",
13+
qnn_third_party_dep("api"),
14+
],
15+
)

0 commit comments

Comments
 (0)