Skip to content

Commit 828b5ea

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 828b5ea

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,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,58 @@ 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__) || \
50+
(defined(__has_feature) && __has_feature(address_sanitizer))
51+
#define ET_TEST_ASAN_ENABLED 1
52+
#else
53+
#define ET_TEST_ASAN_ENABLED 0
54+
#endif
55+
56+
#if !ET_TEST_ASAN_ENABLED
57+
// Replaces the global nothrow aligned allocation function for this test binary
58+
// so FileDataLoader's segment allocation can be made to fail on demand. When
59+
// the toggle is off it forwards to the real aligned allocator. We call the
60+
// throwing aligned new inside a try/catch and convert exceptions to nullptr
61+
// to emulate nothrow semantics without recursing into this same nothrow
62+
// overload (calling ::operator new(size, alignment, std::nothrow) here would
63+
// infinite-loop). Memory allocated here is released through the default
64+
// operator delete, which is not replaced.
65+
// Marked weak to avoid conflict with ASAN runtime which provides its own strong
66+
// definition. Under ASAN the test is skipped.
67+
ET_WEAK
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)