Skip to content

Commit 8569251

Browse files
metascroyfacebook-github-bot
authored andcommitted
Fix throwing operator new in ExecuTorch FileDataLoader (#20400)
Summary: `et_aligned_alloc` in file_data_loader.cpp used the throwing form of `::operator new(size, alignment)`, which throws `std::bad_alloc` on allocation failure. The ExecuTorch runtime is built exception-free and uses a `Result<>`/`Error` convention: `FileDataLoader::load` already guards with `if (aligned_buffer == nullptr) return Error::MemoryAllocationFailed;`, but that null-check was dead code because the throwing `operator new` never returns null. On memory-constrained iOS devices, loading a large model segment (e.g. SceneX V7) could throw, unwind with no landing pad, and abort the process (SIGABRT) -- see the linked task. Switch to the nothrow form `::operator new(size, alignment, std::nothrow)` so the existing null-check fires and `load` returns `Error::MemoryAllocationFailed`, which propagates cleanly through `Program::LoadSegment` -> `Method::init` -> `-[ExecuTorchModule loadMethod:error:]` and surfaces as a catchable NSError instead of aborting. Also add a unit test that forces the segment allocation to fail (by replacing the global nothrow aligned operator new for the test binary) and asserts `load` returns `Error::MemoryAllocationFailed`. Reviewed By: Gasoonjia Differential Revision: D109072812
1 parent 63b4c4d commit 8569251

2 files changed

Lines changed: 96 additions & 1 deletion

File tree

extension/data_loader/file_data_loader.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ namespace extension {
4444

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

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

extension/data_loader/test/file_data_loader_test.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
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

1517
#include <executorch/extension/testing_util/temp_file.h>
1618
#include <executorch/runtime/core/result.h>
19+
#include <executorch/runtime/platform/compiler.h>
1720
#include <executorch/runtime/platform/runtime.h>
1821
#include <executorch/test/utils/alignment.h>
1922

@@ -25,6 +28,53 @@ using executorch::runtime::Error;
2528
using executorch::runtime::FreeableBuffer;
2629
using executorch::runtime::Result;
2730

31+
namespace {
32+
// When set, the replacement nothrow aligned operator new below returns nullptr,
33+
// simulating an allocation failure without needing a real OOM.
34+
std::atomic<bool> g_fail_aligned_nothrow_alloc{false};
35+
36+
// RAII guard to ensure flag is reset even if test asserts early.
37+
struct FailAllocGuard {
38+
FailAllocGuard() {
39+
g_fail_aligned_nothrow_alloc.store(true, std::memory_order_relaxed);
40+
}
41+
~FailAllocGuard() {
42+
g_fail_aligned_nothrow_alloc.store(false, std::memory_order_relaxed);
43+
}
44+
};
45+
} // namespace
46+
47+
// Detect ASAN to avoid multiple definition link error and to skip test when
48+
// ASAN runtime provides its own strong operator new.
49+
#if defined(__SANITIZE_ADDRESS__) || (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. Memory allocated
59+
// here is released through the default operator delete, which is not replaced.
60+
// Marked weak to avoid conflict with ASAN runtime which provides its own strong
61+
// definition. Under ASAN the test is skipped.
62+
ET_WEAK
63+
void* operator new(
64+
std::size_t size,
65+
std::align_val_t alignment,
66+
const std::nothrow_t& /* tag */) noexcept {
67+
if (g_fail_aligned_nothrow_alloc.load(std::memory_order_relaxed)) {
68+
return nullptr;
69+
}
70+
try {
71+
return ::operator new(size, alignment);
72+
} catch (...) {
73+
return nullptr;
74+
}
75+
}
76+
#endif // !ET_TEST_ASAN_ENABLED
77+
2878
class FileDataLoaderTest : public ::testing::TestWithParam<size_t> {
2979
protected:
3080
void SetUp() override {
@@ -147,6 +197,46 @@ TEST_P(FileDataLoaderTest, OutOfBoundsLoadFails) {
147197
}
148198
}
149199

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

0 commit comments

Comments
 (0)