Skip to content

Commit 2b1ccd4

Browse files
authored
Fix throwing operator new in ExecuTorch FileDataLoader (#20400)
Differential Revision: D109072812 Pull Request resolved: #20400
1 parent 39c0df6 commit 2b1ccd4

2 files changed

Lines changed: 102 additions & 1 deletion

File tree

extension/data_loader/file_data_loader.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <cstddef>
1414
#include <cstring>
1515
#include <limits>
16+
#include <new>
1617

1718
#include <executorch/runtime/platform/compat_unistd.h>
1819
#include <fcntl.h>
@@ -44,7 +45,12 @@ namespace extension {
4445

4546
namespace {
4647
inline void* et_aligned_alloc(size_t size, std::align_val_t alignment) {
47-
return ::operator new(size, alignment);
48+
// Use the nothrow form so allocation failure returns nullptr instead of
49+
// throwing std::bad_alloc. ExecuTorch is built exception-free and callers
50+
// (e.g. FileDataLoader::load) check for nullptr and return
51+
// Error::MemoryAllocationFailed; a throw here would unwind with no landing
52+
// pad and abort the process.
53+
return ::operator new(size, alignment, std::nothrow);
4854
}
4955

5056
inline void et_aligned_free(void* ptr, std::align_val_t alignment) {

extension/data_loader/test/file_data_loader_test.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
#include <executorch/extension/data_loader/file_data_loader.h>
1010

11+
#include <atomic>
1112
#include <cstring>
13+
#include <new>
1214

1315
#include <gtest/gtest.h>
1416

@@ -25,6 +27,59 @@ using executorch::runtime::Error;
2527
using executorch::runtime::FreeableBuffer;
2628
using executorch::runtime::Result;
2729

30+
namespace {
31+
// When set, the replacement nothrow aligned operator new below returns nullptr,
32+
// simulating an allocation failure without needing a real OOM.
33+
std::atomic<bool> g_fail_aligned_nothrow_alloc{false};
34+
35+
// RAII guard to ensure flag is reset even if test asserts early.
36+
struct FailAllocGuard {
37+
FailAllocGuard() {
38+
g_fail_aligned_nothrow_alloc.store(true, std::memory_order_relaxed);
39+
}
40+
~FailAllocGuard() {
41+
g_fail_aligned_nothrow_alloc.store(false, std::memory_order_relaxed);
42+
}
43+
};
44+
} // namespace
45+
46+
// Detect ASAN to avoid multiple definition link error and to skip test when
47+
// ASAN runtime provides its own strong operator new.
48+
#if defined(__SANITIZE_ADDRESS__) || \
49+
(defined(__has_feature) && __has_feature(address_sanitizer))
50+
#define ET_TEST_ASAN_ENABLED 1
51+
#else
52+
#define ET_TEST_ASAN_ENABLED 0
53+
#endif
54+
55+
#if !ET_TEST_ASAN_ENABLED
56+
// Replaces the global nothrow aligned allocation function for this test binary
57+
// so FileDataLoader's segment allocation can be made to fail on demand. When
58+
// the toggle is off it forwards to the real aligned allocator. We call the
59+
// throwing aligned new inside a try/catch and convert exceptions to nullptr
60+
// to emulate nothrow semantics without recursing into this same nothrow
61+
// overload (calling ::operator new(size, alignment, std::nothrow) here would
62+
// infinite-loop). Memory allocated here is released through the default
63+
// operator delete, which is not replaced.
64+
// This is a strong (non-weak) replacement so it reliably overrides libc++'s
65+
// default on all platforms (a weak definition loses to libc++'s own weak
66+
// definition on Apple's linker, leaving the override silently unused). Under
67+
// ASAN this whole block is excluded so it can't clash with ASAN's allocator.
68+
void* operator new(
69+
std::size_t size,
70+
std::align_val_t alignment,
71+
const std::nothrow_t& /* tag */) noexcept {
72+
if (g_fail_aligned_nothrow_alloc.load(std::memory_order_relaxed)) {
73+
return nullptr;
74+
}
75+
try {
76+
return ::operator new(size, alignment);
77+
} catch (...) {
78+
return nullptr;
79+
}
80+
}
81+
#endif // !ET_TEST_ASAN_ENABLED
82+
2883
class FileDataLoaderTest : public ::testing::TestWithParam<size_t> {
2984
protected:
3085
void SetUp() override {
@@ -147,6 +202,46 @@ TEST_P(FileDataLoaderTest, OutOfBoundsLoadFails) {
147202
}
148203
}
149204

205+
#if !ET_TEST_ASAN_ENABLED
206+
TEST_P(FileDataLoaderTest, AllocationFailureDuringLoadReturnsError) {
207+
// Create a temp file; contents don't matter.
208+
uint8_t data[256] = {};
209+
TempFile tf(data, sizeof(data));
210+
211+
Result<FileDataLoader> fdl =
212+
FileDataLoader::from(tf.path().c_str(), alignment());
213+
ASSERT_EQ(fdl.error(), Error::Ok);
214+
215+
// Force the segment allocation inside load() to fail. The loader must surface
216+
// Error::MemoryAllocationFailed rather than letting std::bad_alloc escape,
217+
// which would abort the process in the exception-free runtime.
218+
FailAllocGuard fail_guard;
219+
Result<FreeableBuffer> fb = fdl->load(
220+
/*offset=*/0,
221+
/*size=*/sizeof(data),
222+
DataLoader::SegmentInfo(DataLoader::SegmentInfo::Type::Program));
223+
224+
EXPECT_EQ(fb.error(), Error::MemoryAllocationFailed);
225+
}
226+
#endif // !ET_TEST_ASAN_ENABLED
227+
228+
#if !ET_TEST_ASAN_ENABLED
229+
TEST_P(FileDataLoaderTest, AllocationFailureDuringFromReturnsError) {
230+
// Create a temp file; contents don't matter.
231+
uint8_t data[256] = {};
232+
TempFile tf(data, sizeof(data));
233+
234+
// Force the filename allocation inside from() to fail. FileDataLoader::from
235+
// copies the filename using et_aligned_alloc and must return
236+
// Error::MemoryAllocationFailed on nullptr rather than throwing.
237+
FailAllocGuard fail_guard;
238+
Result<FileDataLoader> fdl =
239+
FileDataLoader::from(tf.path().c_str(), alignment());
240+
241+
EXPECT_EQ(fdl.error(), Error::MemoryAllocationFailed);
242+
}
243+
#endif // !ET_TEST_ASAN_ENABLED
244+
150245
TEST_P(FileDataLoaderTest, FromMissingFileFails) {
151246
// Wrapping a file that doesn't exist should fail.
152247
Result<FileDataLoader> fdl = FileDataLoader::from(

0 commit comments

Comments
 (0)