Skip to content

Commit f502620

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 f502620

2 files changed

Lines changed: 101 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: 94 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,57 @@ 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. 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+
// Marked weak to avoid conflict with ASAN runtime which provides its own strong
65+
// definition. Under ASAN the test is skipped.
66+
ET_WEAK
67+
void* operator new(
68+
std::size_t size,
69+
std::align_val_t alignment,
70+
const std::nothrow_t& /* tag */) noexcept {
71+
if (g_fail_aligned_nothrow_alloc.load(std::memory_order_relaxed)) {
72+
return nullptr;
73+
}
74+
try {
75+
return ::operator new(size, alignment);
76+
} catch (...) {
77+
return nullptr;
78+
}
79+
}
80+
#endif // !ET_TEST_ASAN_ENABLED
81+
2882
class FileDataLoaderTest : public ::testing::TestWithParam<size_t> {
2983
protected:
3084
void SetUp() override {
@@ -147,6 +201,46 @@ TEST_P(FileDataLoaderTest, OutOfBoundsLoadFails) {
147201
}
148202
}
149203

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

0 commit comments

Comments
 (0)