Skip to content

Commit c4eef80

Browse files
committed
Fix ASAN problems under Clang 15.
It appears ASAN now by default tries to detect stack-use-after-return. This breaks our assumptions in requireOnStack() and totally breaks fibers. For requireOnStack() we can just skip the check in this case. For fibers, we need to implement the ASAN hints to tell it when we're switching fibers.
1 parent 0ef5937 commit c4eef80

File tree

4 files changed

+92
-8
lines changed

4 files changed

+92
-8
lines changed

c++/src/kj/async-test.c++

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,7 +1162,12 @@ KJ_TEST("fiber pool") {
11621162
if (i1_local == nullptr) {
11631163
i1_local = &i;
11641164
} else {
1165+
#if !KJ_HAS_COMPILER_FEATURE(address_sanitizer)
1166+
// Verify that the stack variable is in the exact same spot as before.
1167+
// May not work under ASAN as the instrumentation to detect stack-use-after-return can
1168+
// change the address.
11651169
KJ_ASSERT(i1_local == &i);
1170+
#endif
11661171
}
11671172
return i;
11681173
});
@@ -1173,7 +1178,9 @@ KJ_TEST("fiber pool") {
11731178
if (i2_local == nullptr) {
11741179
i2_local = &i;
11751180
} else {
1181+
#if !KJ_HAS_COMPILER_FEATURE(address_sanitizer)
11761182
KJ_ASSERT(i2_local == &i);
1183+
#endif
11771184
}
11781185
return i;
11791186
});
@@ -1210,9 +1217,24 @@ KJ_TEST("fiber pool") {
12101217
bool onOurStack(char* p) {
12111218
// If p points less than 64k away from a random stack variable, then it must be on the same
12121219
// stack, since we never allocate stacks smaller than 64k.
1220+
#if KJ_HAS_COMPILER_FEATURE(address_sanitizer)
1221+
// The stack-use-after-return detection mechanism breaks our ability to check this, so don't.
1222+
return true;
1223+
#else
12131224
char c;
12141225
ptrdiff_t diff = p - &c;
12151226
return diff < 65536 && diff > -65536;
1227+
#endif
1228+
}
1229+
1230+
bool notOnOurStack(char* p) {
1231+
// Opposite of onOurStack(), except returns true if the check can't be performed.
1232+
#if KJ_HAS_COMPILER_FEATURE(address_sanitizer)
1233+
// The stack-use-after-return detection mechanism breaks our ability to check this, so don't.
1234+
return true;
1235+
#else
1236+
return !onOurStack(p);
1237+
#endif
12161238
}
12171239

12181240
KJ_TEST("fiber pool runSynchronously()") {
@@ -1240,11 +1262,14 @@ KJ_TEST("fiber pool runSynchronously()") {
12401262
});
12411263
KJ_ASSERT(ptr2 != nullptr);
12421264

1265+
#if !KJ_HAS_COMPILER_FEATURE(address_sanitizer)
12431266
// Should have used the same stack both times, so local var would be in the same place.
1267+
// Under ASAN, the stack-use-after-return detection correctly fires on this, so we skip the check.
12441268
KJ_EXPECT(ptr1 == ptr2);
1269+
#endif
12451270

12461271
// Should have been on a different stack from the main stack.
1247-
KJ_EXPECT(!onOurStack(ptr1));
1272+
KJ_EXPECT(notOnOurStack(ptr1));
12481273

12491274
KJ_EXPECT_THROW_MESSAGE("test exception",
12501275
pool.runSynchronously([&]() { KJ_FAIL_ASSERT("test exception"); }));
@@ -1298,7 +1323,7 @@ KJ_TEST("fiber pool limit") {
12981323
// is the one from the thread.
12991324
pool.runSynchronously([&]() {
13001325
KJ_EXPECT(onOurStack(ptr2));
1301-
KJ_EXPECT(!onOurStack(ptr1));
1326+
KJ_EXPECT(notOnOurStack(ptr1));
13021327

13031328
KJ_EXPECT(pool.getFreelistSize() == 0);
13041329
});
@@ -1369,15 +1394,15 @@ KJ_TEST("run event loop on freelisted stacks") {
13691394

13701395
// The event callbacks should have run on a different stack, but the wait should have been on
13711396
// the main stack.
1372-
KJ_EXPECT(!onOurStack(ptr1));
1373-
KJ_EXPECT(!onOurStack(ptr2));
1397+
KJ_EXPECT(notOnOurStack(ptr1));
1398+
KJ_EXPECT(notOnOurStack(ptr2));
13741399
KJ_EXPECT(onOurStack(port.waitStack));
13751400

13761401
pool.runSynchronously([&]() {
13771402
// This should run on the same stack where the event callbacks ran.
13781403
KJ_EXPECT(onOurStack(ptr1));
13791404
KJ_EXPECT(onOurStack(ptr2));
1380-
KJ_EXPECT(!onOurStack(port.waitStack));
1405+
KJ_EXPECT(notOnOurStack(port.waitStack));
13811406
});
13821407
}
13831408

@@ -1410,8 +1435,8 @@ KJ_TEST("run event loop on freelisted stacks") {
14101435

14111436
// The event callback should have run on a different stack, and poll() should have run on
14121437
// a separate stack too.
1413-
KJ_EXPECT(!onOurStack(ptr1));
1414-
KJ_EXPECT(!onOurStack(port.pollStack));
1438+
KJ_EXPECT(notOnOurStack(ptr1));
1439+
KJ_EXPECT(notOnOurStack(port.pollStack));
14151440

14161441
pool.runSynchronously([&]() {
14171442
// This should run on the same stack where the event callbacks ran.

c++/src/kj/async.c++

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@
7878

7979
#include <stdlib.h>
8080

81+
#if KJ_HAS_COMPILER_FEATURE(address_sanitizer)
82+
// Clang's address sanitizer requires special hints when switching fibers, especially in order for
83+
// stack-use-after-return handling to work right.
84+
//
85+
// TODO(someday): Does GCC's sanitizer, flagged by __SANITIZE_ADDRESS__, have these hints too? I
86+
// don't know and am not in a position to test, so I'm assuming not for now.
87+
#include <sanitizer/common_interface_defs.h>
88+
#else
89+
// Nop the hints so that we don't have to put #ifdefs around every use.
90+
#define __sanitizer_start_switch_fiber(...)
91+
#define __sanitizer_finish_switch_fiber(...)
92+
#endif
93+
8194
#if _MSC_VER && !__clang__
8295
// MSVC's atomic intrinsics are weird and different, whereas the C++ standard atomics match the GCC
8396
// builtins -- except for requiring the obnoxious std::atomic<T> wrapper. So, on MSVC let's just
@@ -1330,6 +1343,23 @@ struct FiberStack::Impl {
13301343
jmp_buf fiberJmpBuf;
13311344
jmp_buf originalJmpBuf;
13321345

1346+
#if KJ_HAS_COMPILER_FEATURE(address_sanitizer)
1347+
// Stuff that we need to pass to __sanitizer_start_switch_fiber() /
1348+
// __sanitizer_finish_switch_fiber() when using ASAN.
1349+
1350+
void* originalFakeStack = nullptr;
1351+
void* fiberFakeStack = nullptr;
1352+
// Pointer to ASAN "fake stack" associated with the fiber and its calling stack. Filled in by
1353+
// __sanitizer_start_switch_fiber() before switching away, consumed by
1354+
// __sanitizer_finish_switch_fiber() upon switching back.
1355+
1356+
void const* originalBottom;
1357+
size_t originalSize;
1358+
// Size and location of the original stack before switching fibers. These are filled in by
1359+
// __sanitizer_finish_switch_fiber() after the switch, and must be passed to
1360+
// __sanitizer_start_switch_fiber() when switching back later.
1361+
#endif
1362+
13331363
static Impl* alloc(size_t stackSize, ucontext_t* context) {
13341364
#ifndef MAP_ANONYMOUS
13351365
#define MAP_ANONYMOUS MAP_ANON
@@ -1423,6 +1453,9 @@ struct FiberStack::StartRoutine {
14231453

14241454
auto& stack = *reinterpret_cast<FiberStack*>(ptr);
14251455

1456+
__sanitizer_finish_switch_fiber(nullptr,
1457+
&stack.impl->originalBottom, &stack.impl->originalSize);
1458+
14261459
// We first switch to the fiber inside of the FiberStack constructor. This is just for
14271460
// initialization purposes, and we're expected to switch back immediately.
14281461
stack.switchToMain();
@@ -1478,9 +1511,11 @@ FiberStack::FiberStack(size_t stackSizeParam)
14781511

14791512
makecontext(&context, reinterpret_cast<void(*)()>(&StartRoutine::run), 2, arg1, arg2);
14801513

1514+
__sanitizer_start_switch_fiber(&impl->originalFakeStack, impl, stackSize - sizeof(Impl));
14811515
if (_setjmp(impl->originalJmpBuf) == 0) {
14821516
setcontext(&context);
14831517
}
1518+
__sanitizer_finish_switch_fiber(impl->originalFakeStack, nullptr, nullptr);
14841519
#endif
14851520
#else
14861521
#if KJ_NO_EXCEPTIONS
@@ -1577,9 +1612,11 @@ void FiberStack::switchToFiber() {
15771612
#if _WIN32 || __CYGWIN__
15781613
SwitchToFiber(osFiber);
15791614
#else
1615+
__sanitizer_start_switch_fiber(&impl->originalFakeStack, impl, stackSize - sizeof(Impl));
15801616
if (_setjmp(impl->originalJmpBuf) == 0) {
15811617
_longjmp(impl->fiberJmpBuf, 1);
15821618
}
1619+
__sanitizer_finish_switch_fiber(impl->originalFakeStack, nullptr, nullptr);
15831620
#endif
15841621
#endif
15851622
}
@@ -1590,9 +1627,21 @@ void FiberStack::switchToMain() {
15901627
#if _WIN32 || __CYGWIN__
15911628
SwitchToFiber(getMainWin32Fiber());
15921629
#else
1630+
// TODO(someady): In theory, the last time we switch away from the fiber, we should pass `nullptr`
1631+
// for the first argument here, so that ASAN destroys the fake stack. However, as currently
1632+
// designed, we don't actually know if we're switching away for the last time. It's understood
1633+
// that when we call switchToMain() in FiberStack::run(), then the main stack is allowed to
1634+
// destroy the fiber, or reuse it. I don't want to develop a mechanism to switch back to the
1635+
// fiber on final destruction just to get the hints right, so instead we leak the fake stack.
1636+
// This doesn't seem to cause any problems -- it's not even detected by ASAN as a memory leak.
1637+
// But if we wanted to run ASAN builds in production or something, it might be an issue.
1638+
__sanitizer_start_switch_fiber(&impl->fiberFakeStack,
1639+
impl->originalBottom, impl->originalSize);
15931640
if (_setjmp(impl->fiberJmpBuf) == 0) {
15941641
_longjmp(impl->originalJmpBuf, 1);
15951642
}
1643+
__sanitizer_finish_switch_fiber(impl->fiberFakeStack,
1644+
&impl->originalBottom, &impl->originalSize);
15961645
#endif
15971646
#endif
15981647
}

c++/src/kj/exception-test.c++

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,16 @@ TEST(Exception, UnwindDetector) {
132132
}
133133
#endif
134134

135+
#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) || \
136+
KJ_HAS_COMPILER_FEATURE(address_sanitizer) || \
137+
defined(__SANITIZE_ADDRESS__)
138+
// The implementation skips this check in these cases.
139+
#else
135140
#if !__MINGW32__ // Inexplicably crashes when exception is thrown from constructor.
136141
TEST(Exception, ExceptionCallbackMustBeOnStack) {
137142
KJ_EXPECT_THROW_MESSAGE("must be allocated on the stack", new ExceptionCallback);
138143
}
144+
#endif
139145
#endif // !__MINGW32__
140146

141147
#if !KJ_NO_EXCEPTIONS

c++/src/kj/exception.c++

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,11 @@ KJ_THREADLOCAL_PTR(ExceptionCallback) threadLocalCallback = nullptr;
971971

972972
ExceptionCallback::ExceptionCallback(): next(getExceptionCallback()) {
973973
char stackVar;
974-
#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
974+
#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) || \
975+
KJ_HAS_COMPILER_FEATURE(address_sanitizer) || \
976+
defined(__SANITIZE_ADDRESS__)
977+
// When using libfuzzer or ASAN, this sanity check may spurriously fail, so skip it.
978+
#else
975979
ptrdiff_t offset = reinterpret_cast<char*>(this) - &stackVar;
976980
KJ_ASSERT(offset < 65536 && offset > -65536,
977981
"ExceptionCallback must be allocated on the stack.");

0 commit comments

Comments
 (0)