Skip to content

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

koxudaxi
Copy link

@koxudaxi koxudaxi commented Jul 9, 2025

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 sanitizer
macros are present.

Files touched

  • py/mpconfig.h – default margin for builds that define the sanitizer macros
  • ports/unix/mpconfigport.h – same margin applied to the unix port
  • tests/thread/thread_stacksize1.py – larger stack requested on desktop
    targets where sanitizers are common
  • docs/library/micropython.rst – short note explaining the new default

Builds without sanitizers behave exactly as before.

Testing

  • unix port (macOS, Clang 18.1)
    • normal build: tools/run-tests.py → all tests pass
    • -fsanitize=address,undefined:
      • tools/run-tests.py tests/thread/thread_stacksize1.py passes
      • no premature “stack overflow” exceptions
      • UBSan prints the same null-pointer warnings that are already present on
        the main branch; they are unrelated to this change
  • Other ports were not rebuilt; they keep the default margin of zero and are
    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 a
different value is required.

Copy link

codecov bot commented Jul 9, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.44%. Comparing base (df05cae) to head (d1ff01d).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@koxudaxi koxudaxi force-pushed the fix/tool-sanitizers branch from b5d5f25 to 873da78 Compare July 9, 2025 18:27
Copy link

github-actions bot commented Jul 9, 2025

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:   +16 +0.002% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

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>
@koxudaxi koxudaxi force-pushed the fix/tool-sanitizers branch from 873da78 to d1ff01d Compare July 9, 2025 18:43
@projectgus projectgus self-requested a review July 11, 2025 07:40
@dpgeorge
Copy link
Member

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.

@AJMansfield
Copy link
Contributor

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...

the compiler enlarges each C call frame; MicroPython’s stack-overflow guard can therefore trigger before the real end of the stack.

This is actually the opposite of what I believe is happening. Setting MICROPY_STACK_CHECK_MARGIN to a larger value makes micropython's stack-checking more paranoid, i.e. larger values make it trigger earlier, while the stack is still smaller.

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:
What is is micropython actually doing that would convince a sanitizer that some of its stack accesses are out-of-bounds?

Copy link
Contributor

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.

Copy link
Contributor

@projectgus projectgus left a 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
Copy link
Contributor

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.)

@koxudaxi
Copy link
Author

@dpgeorge @AJMansfield @projectgus
Thank you for reviewing this PR.
During the last two days I fixed some bugs and added tests on the t‑strings branch. After this work my view on this patch changed.
Now t‑strings is enabled only on ports with a larger stack (macOS, WebAssembly, Windows, etc.). In these builds the tests pass without changing MICROPY_STACK_CHECK_MARGIN, so the PR looks unnecessary.
But when I made a test with many interpolations, I still got a stack‑overflow in a sanitizer build. On smaller targets we avoid this by disabling the feature, but it might point to a hidden issue between sanitizer builds and our stack checker.
Should I keep improving this PR based on the review, or should we close it because we don’t need it now?
Feedback from people who often run sanitizer builds would be very helpful.

@projectgus
Copy link
Contributor

@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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy