-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
build/sanitizer: reserve 8 KiB C-stack margin for sanitizer builds #17649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #17649 +/- ##
=======================================
Coverage 98.44% 98.44%
=======================================
Files 171 171
Lines 22192 22192
=======================================
Hits 21847 21847
Misses 345 345 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
b5d5f25
to
873da78
Compare
Code size report:
|
Compiler sanitizers such as AddressSanitizer and UndefinedSanitizer inflate call frames and can trip MicroPython's C-stack overflow check. Define MICROPY_STACK_CHECK_MARGIN = 8192 when the relevant sanitizer macros are present, update the unix port config, docs, and the thread_stacksize test. Non-sanitizer builds are unchanged. Signed-off-by: Koudai Aono <koxudaxi@gmail.com>
873da78
to
d1ff01d
Compare
Thanks for the patch. Can you describe a test which shows that this is necessary? The CI has been passing, and it does recursive depth tests, so I wonder why it's needed. |
I'm not very confident I understand what's going on here, as I've not been able to figure out how to successfully link a clang build of micropython with the asan and ubsan sanitizers in question on linux, but after digging in on what the stack check margin actually does...
This is actually the opposite of what I believe is happening. Setting inline static void mp_cstack_init_with_top(void *top, size_t stack_size) {
MP_STATE_THREAD(stack_top) = (char *)top;
#if MICROPY_STACK_CHECK
assert(stack_size > MICROPY_STACK_CHECK_MARGIN); // Should be enforced by port
MP_STATE_THREAD(stack_limit) = stack_size - MICROPY_STACK_CHECK_MARGIN;
#else
(void)stack_size;
#endif
} void mp_cstack_check(void) {
if (mp_cstack_usage() >= MP_STATE_THREAD(stack_limit)) {
mp_raise_recursion_depth();
}
} MP_NORETURN void mp_raise_recursion_depth(void) {
mp_raise_type_arg(&mp_type_RuntimeError, MP_OBJ_NEW_QSTR(MP_QSTR_maximum_space_recursion_space_depth_space_exceeded));
} Which then leaves margin for C functions called between the stack checks to use up to 8192 bytes more than the specified stack limit, before those operations actually start to overrun --- i.e. converting a situation that might result in a hard stack overflow, or memory corruption on platforms without stack guard pages and the like. But that still leaves an unanswered question: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test change makes sense though --- if the sanitizer is padding out every stack frame with guard pages, then in a thread context where the standard library can't just freely syscall brk
to expand, it makes sense that the test might need to explicitly request larger stacks that can hold all the extra padding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a reasonable change, thanks @koxudaxi! Although I'm also curious how to reproduce a stack-related sanitizer failure on the current master.
#define MICROPY_STACK_CHECK_MARGIN (8192) | ||
#endif | ||
#endif | ||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While we only support sanitizers on the unix port does it makes sense to move this block into ports/unix/mpconfigport.h
?
(It's also redundant in this version of the PR, where the margin is already added unconditionally on the unix port.)
@dpgeorge @AJMansfield @projectgus |
@koxudaxi Appreciate the update, thanks. I think that until someone encounters this problem again then we should close this, but keep this PR in mind if they do run into stack issues when using sanitizers. (Others may recommend differently, though.) |
Summary
This PR moves the sanitizer-only changes that reviewers asked to split out of
#17557.
When a build is compiled with AddressSanitizer or UndefinedSanitizer the
compiler enlarges each C call frame; MicroPython’s stack-overflow guard can
therefore trigger before the real end of the stack. To avoid that, the patch
sets
MICROPY_STACK_CHECK_MARGIN
to 8192 bytes whenever the usual sanitizermacros are present.
Files touched
py/mpconfig.h
– default margin for builds that define the sanitizer macrosports/unix/mpconfigport.h
– same margin applied to the unix porttests/thread/thread_stacksize1.py
– larger stack requested on desktoptargets where sanitizers are common
docs/library/micropython.rst
– short note explaining the new defaultBuilds without sanitizers behave exactly as before.
Testing
tools/run-tests.py
→ all tests pass-fsanitize=address,undefined
:tools/run-tests.py tests/thread/thread_stacksize1.py
passesthe main branch; they are unrelated to this change
unaffected.
Trade-offs and alternatives
Sanitizer builds lose 8 KiB of usable C stack. A smaller margin (4 KiB) was
occasionally insufficient in deep recursion tests; larger values offered no
extra benefit. Each port can still override
MICROPY_STACK_CHECK_MARGIN
if adifferent value is required.