From a4275e23572f4c8b71f41859b268ce67bc5ce1de Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Tue, 1 Sep 2026 16:35:28 -0700 Subject: [PATCH 1/2] Add an asan native_sim build And fix a couple issues it found. --- ports/zephyr-cp/Makefile | 17 ++- ports/zephyr-cp/boards/board_aliases.cmake | 1 + .../native_sim_asan/autogen_board_info.toml | 131 ++++++++++++++++++ .../boards/native/native_sim_asan/board.conf | 51 +++++++ .../native/native_sim_asan/board.overlay | 8 ++ .../native/native_sim_asan/circuitpython.toml | 1 + ports/zephyr-cp/supervisor/port.c | 35 ++++- ports/zephyr-cp/tests/conftest.py | 17 ++- ports/zephyr-cp/tests/test_basics.py | 4 +- ports/zephyr-cp/zephyr_main.c | 14 ++ shared-module/terminalio/Terminal.c | 33 +++-- shared/runtime/stdout_helpers.c | 3 +- 12 files changed, 297 insertions(+), 18 deletions(-) create mode 100644 ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml create mode 100644 ports/zephyr-cp/boards/native/native_sim_asan/board.conf create mode 100644 ports/zephyr-cp/boards/native/native_sim_asan/board.overlay create mode 100644 ports/zephyr-cp/boards/native/native_sim_asan/circuitpython.toml diff --git a/ports/zephyr-cp/Makefile b/ports/zephyr-cp/Makefile index fbde791afa9..95bfa646676 100644 --- a/ports/zephyr-cp/Makefile +++ b/ports/zephyr-cp/Makefile @@ -123,6 +123,21 @@ sim: clean-sim: rm -rf $(wildcard build-native_*) -test: build-native_native_sim/zephyr-cp/zephyr/zephyr.exe +# Every board the test suite uses: native sim (non-asan + asan) and the bsim +# boards (tests/bsim/conftest.py parametrizes over both). Zephyr samples for +# bsim tests are built on demand by the zephyr_sample fixture. +TEST_BOARDS := native_native_sim native_native_sim_asan native_nrf5340bsim native_nrf54lm20bsim + +# Delegate to a sub-make with BOARD set so the per-board build rule (and its +# shield args + bsim prep) applies. The targets are phony: the west builds are +# incremental, so this just keeps the test binaries fresh. +define test_board_rule +.PHONY: test-build-$(1) +test-build-$(1): + +$$(MAKE) BOARD=$(1) BUILD=build-$(1) build-$(1)/zephyr-cp/zephyr/zephyr.elf +endef +$(foreach board,$(TEST_BOARDS),$(eval $(call test_board_rule,$(board)))) + +test: $(TEST_BOARDS:%=test-build-%) pytest cptools/tests pytest tests/ -v diff --git a/ports/zephyr-cp/boards/board_aliases.cmake b/ports/zephyr-cp/boards/board_aliases.cmake index 29ce83ceb5e..e1faa7c3888 100644 --- a/ports/zephyr-cp/boards/board_aliases.cmake +++ b/ports/zephyr-cp/boards/board_aliases.cmake @@ -35,6 +35,7 @@ cp_board_alias(renesas_ek_ra6m5 ek_ra6m5) cp_board_alias(renesas_ek_ra8d1 ek_ra8d1) cp_board_alias(renesas_da14695_dk_usb da14695_dk_usb) cp_board_alias(native_native_sim native_sim/native) +cp_board_alias(native_native_sim_asan native_sim/native) cp_board_alias(native_nrf5340bsim nrf5340bsim/nrf5340/cpuapp) cp_board_alias(native_nrf54lm20bsim nrf54lm20bsim/nrf54lm20a/cpuapp) cp_board_alias(nordic_nrf54l15dk nrf54l15dk/nrf54l15/cpuapp) diff --git a/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml new file mode 100644 index 00000000000..6ce71ece57f --- /dev/null +++ b/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml @@ -0,0 +1,131 @@ +# This file is autogenerated when a board is built. Do not edit. Do commit it to git. Other scripts use its info. +name = "zephyr Native simulator - native_sim" + +[modules] +__future__ = true +_bleio = false +_eve = false +_pew = false +_pixelmap = false +_stage = false +adafruit_bus_device = true +adafruit_pixelbuf = false +aesio = true +alarm = false +analogbufio = false +analogio = false +atexit = false +audiobusio = true # Zephyr board has audiobusio +audiocore = true # Zephyr board has audiobusio +audiodelays = true # Zephyr board has audiobusio +audiofilewriter = false +audiofilters = true # Zephyr board has audiobusio +audiofreeverb = true # Zephyr board has audiobusio +audioi2sin = false +audioio = false +audiomixer = true # Zephyr board has audiobusio +audiomp3 = true # Zephyr board has audiobusio +audiopwmio = false +audiospeed = false +aurora_epaper = false +bitbangio = false +bitmapfilter = true # Zephyr board has busio +bitmaptools = true # Zephyr board has busio +bitops = false +board = false +busdisplay = true # Zephyr board has busio +busio = true # Zephyr board has busio +camera = false +canio = false +codeop = false +countio = false +digitalio = true +displayio = true # Zephyr board has displayio +dotclockframebuffer = false +dualbank = false +emmcio = false +epaperdisplay = true # Zephyr board has busio +floppyio = false +fontio = true # Zephyr board has busio +fourwire = true # Zephyr board has busio +framebufferio = true # Zephyr board has busio +frequencyio = false +getpass = true +gifio = true # Zephyr board has busio +gnss = false +hashlib = true # Zephyr networking enabled +hostnetwork = true # Zephyr board has hostnetwork +i2cdisplaybus = true # Zephyr board has busio +i2cioexpander = false +i2ctarget = false +imagecapture = false +ipaddress = true # Zephyr networking enabled +is31fl3741 = false +jpegio = true # Zephyr board has busio +keypad = false +keypad_demux = false +locale = false +lvfontio = true # Zephyr board has busio +math = true +max3421e = false +mcp4822 = false +mdns = false +memorymap = false +memorymonitor = false +microcontroller = true +mipidsi = false +msgpack = true +neopixel_write = false +nvm = true # Zephyr board has nvm +onewireio = false +os = true +paralleldisplaybus = false +picogame = false +ps2io = false +pulseio = false +pwmio = false +qrio = false +qspibus = false +rainbowio = true +random = true +rclcpy = false +rgbmatrix = false +rotaryio = true # Zephyr board has rotaryio +rtc = false +sdcardio = true # Zephyr board has busio +sdioio = false +sharpdisplay = true # Zephyr board has busio +socketpool = true # Zephyr networking enabled +spitarget = false +ssl = false +storage = true +struct = true +supervisor = true +synthio = true # Zephyr board has audiobusio +terminalio = true # Zephyr board has busio +tilepalettemapper = true # Zephyr board has busio +time = true +touchio = false +traceback = true +uheap = false +usb = false +usb_audio = false +usb_cdc = false +usb_hid = false +usb_host = false +usb_midi = false +usb_video = false +ustack = false +vectorio = true # Zephyr board has busio +warnings = true +watchdog = false +wifi = false +zephyr_display = true # Zephyr board has zephyr_display +zephyr_kernel = false +zlib = true +# extmod modules shared with MicroPython +asyncio = true +binascii = true +json = true +re = true +select = true diff --git a/ports/zephyr-cp/boards/native/native_sim_asan/board.conf b/ports/zephyr-cp/boards/native/native_sim_asan/board.conf new file mode 100644 index 00000000000..62ebc476a8e --- /dev/null +++ b/ports/zephyr-cp/boards/native/native_sim_asan/board.conf @@ -0,0 +1,51 @@ +# Build with AddressSanitizer to catch memory errors in tests. +CONFIG_ASAN=y + +# No Bluetooth hardware on native_sim +CONFIG_BT=n + +CONFIG_EMUL=y +CONFIG_GPIO=y +CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n + +# So we can test safe mode +CONFIG_NATIVE_SIM_REBOOT=y + +CONFIG_TRACING=y +CONFIG_TRACING_PERFETTO=y +CONFIG_TRACING_SYNC=y +CONFIG_TRACING_BACKEND_POSIX=y +CONFIG_TRACING_GPIO=y + +# I2C emulation for testing +CONFIG_I2C_EMUL=y + +# Display emulation for display/terminal golden tests. +CONFIG_DISPLAY=y +CONFIG_SDL_DISPLAY=y +# Don't require hardware acceleration so the software renderer can be used. +CONFIG_SDL_DISPLAY_USE_HARDWARE_ACCELERATOR=n + +# EEPROM emulation for testing +CONFIG_EEPROM=y +CONFIG_EEPROM_AT24=y +CONFIG_EEPROM_AT2X_EMUL=y + +# I2S SDL emulation for audio testing +CONFIG_I2S_SDL=y + +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y +CONFIG_ETH_NATIVE_TAP=n +CONFIG_NET_DRIVERS=y +CONFIG_NET_SOCKETS_OFFLOAD=y +CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y +CONFIG_HEAP_MEM_POOL_SIZE=1024 + +CONFIG_NET_LOG=y + +CONFIG_MBEDTLS=y +CONFIG_PSA_WANT_ALG_SHA_1=y +CONFIG_PSA_WANT_ALG_SHA_256=y diff --git a/ports/zephyr-cp/boards/native/native_sim_asan/board.overlay b/ports/zephyr-cp/boards/native/native_sim_asan/board.overlay new file mode 100644 index 00000000000..33cbc530998 --- /dev/null +++ b/ports/zephyr-cp/boards/native/native_sim_asan/board.overlay @@ -0,0 +1,8 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Device tree overlay for CircuitPython on native_sim with AddressSanitizer. + * Identical hardware description to the non-asan board, so just reuse it. + */ + +#include "../native_sim/board.overlay" diff --git a/ports/zephyr-cp/boards/native/native_sim_asan/circuitpython.toml b/ports/zephyr-cp/boards/native/native_sim_asan/circuitpython.toml new file mode 100644 index 00000000000..7177c825f17 --- /dev/null +++ b/ports/zephyr-cp/boards/native/native_sim_asan/circuitpython.toml @@ -0,0 +1 @@ +CIRCUITPY_BUILD_EXTENSIONS = ["exe"] diff --git a/ports/zephyr-cp/supervisor/port.c b/ports/zephyr-cp/supervisor/port.c index 43d42dcaf5c..e26f256bb2b 100644 --- a/ports/zephyr-cp/supervisor/port.c +++ b/ports/zephyr-cp/supervisor/port.c @@ -22,6 +22,7 @@ #if defined(CONFIG_ARCH_POSIX) #include +#include #include #include "cmdline.h" @@ -280,7 +281,39 @@ uint32_t *port_stack_get_limit(void) { uint32_t *port_stack_get_top(void) { _thread_stack_info_t stack_info = k_current_get()->stack_info; - return (uint32_t *)(stack_info.start + stack_info.size - stack_info.delta); + uint32_t *top = (uint32_t *)(stack_info.start + stack_info.size - stack_info.delta); + #if defined(CONFIG_ARCH_POSIX) + // On hosted builds the thread stack is a pthread stack. pthread_getattr_np(), + // which the POSIX arch uses to fix up stack_info, can report a size larger + // than the real mapping (ASan intercepts it and inflates the size). The GC + // scans up to the returned top, so clamp it to the end of the mapping that + // contains the current stack pointer. + // + // Only ever clamp *downwards*: /proc/self/maps merges adjacent anonymous + // mappings with the same flags, so the line containing our stack pointer + // routinely covers several thread stacks at once (e.g. 0xf63c2000-0xf73c4000 + // for two 8MB stacks). Taking its end as the top made the GC scan megabytes + // past this thread's stack, into the neighbouring thread's live stack -- and + // segfault whenever the range had an unmapped hole (a guard page, or a stack + // that has since been freed). + volatile uint32_t stack_probe; + uintptr_t sp = (uintptr_t)&stack_probe; + FILE *maps = fopen("/proc/self/maps", "r"); + if (maps != NULL) { + char line[256]; + unsigned long low, high; + while (fgets(line, sizeof(line), maps) != NULL) { + if (sscanf(line, "%lx-%lx", &low, &high) == 2 && low <= sp && sp < high) { + if (high < (uintptr_t)top) { + top = (uint32_t *)high; + } + break; + } + } + fclose(maps); + } + #endif + return top; } uint64_t port_get_raw_ticks(uint8_t *subticks) { diff --git a/ports/zephyr-cp/tests/conftest.py b/ports/zephyr-cp/tests/conftest.py index 3e8a4f0998c..409732c5c67 100644 --- a/ports/zephyr-cp/tests/conftest.py +++ b/ports/zephyr-cp/tests/conftest.py @@ -149,14 +149,21 @@ def log_uart_trace_output(trace_file: Path) -> None: ) -@pytest.fixture +# Native_sim boards each test runs against: the non-asan default and the +# asan-enabled build, so memory errors fail tests. +NATIVE_BOARDS = ["native_native_sim", "native_native_sim_asan"] + + +@pytest.fixture(params=NATIVE_BOARDS) def board(request): + """Parametrized over both native_sim builds (non-asan and asan). + + The bsim conftest overrides this fixture with its own bsim boards. + """ board = request.node.get_closest_marker("circuitpython_board") if board is not None: - board = board.args[0] - else: - board = "native_native_sim" - return board + return board.args[0] + return request.param @pytest.fixture diff --git a/ports/zephyr-cp/tests/test_basics.py b/ports/zephyr-cp/tests/test_basics.py index e7b96a1f7c9..0120ad660d8 100644 --- a/ports/zephyr-cp/tests/test_basics.py +++ b/ports/zephyr-cp/tests/test_basics.py @@ -7,12 +7,12 @@ @pytest.mark.circuitpy_drive(None) -def test_blank_flash_hello_world(circuitpython): +def test_blank_flash_hello_world(board, circuitpython): """Test that an erased flash shows code.py output header.""" circuitpython.wait_until_done() output = circuitpython.serial.all_output - assert "Board ID:native_native_sim" in output + assert f"Board ID:{board}" in output assert "UID:" in output assert "code.py output:" in output assert "Hello World" in output diff --git a/ports/zephyr-cp/zephyr_main.c b/ports/zephyr-cp/zephyr_main.c index 8b8877c7b11..68afea5e912 100644 --- a/ports/zephyr-cp/zephyr_main.c +++ b/ports/zephyr-cp/zephyr_main.c @@ -2,6 +2,20 @@ extern int circuitpython_main(void); +#if defined(__SANITIZE_ADDRESS__) || (defined(__has_feature) && __has_feature(address_sanitizer)) +// ASAN's stack-use-after-return detection (on by default in recent runtimes) +// moves C locals into "fake stack" frames on the heap. The GC scans the real +// machine stack for live object pointers, so with fake stacks enabled it misses +// pointers held in C frames and collects still-referenced objects, corrupting +// the heap. GCC's instrumentation consults this global at every instrumented +// function entry, so clearing it from a constructor disables only that check; +// all other ASAN checks stay enabled. +extern char __asan_option_detect_stack_use_after_return; +__attribute__((constructor)) static void cp_disable_asan_fake_stack(void) { + __asan_option_detect_stack_use_after_return = 0; +} +#endif + int main(void) { // Use a unique name for CP main so that the linker needs to look in libcircuitpython.a return circuitpython_main(); diff --git a/shared-module/terminalio/Terminal.c b/shared-module/terminalio/Terminal.c index 6e73804ea3a..bd363b5e9ae 100644 --- a/shared-module/terminalio/Terminal.c +++ b/shared-module/terminalio/Terminal.c @@ -201,12 +201,18 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con #endif const byte *i = data; + const byte *end = data + len; uint16_t start_y = self->cursor_y; - while (i < data + len) { + while (i < end) { unichar c = utf8_get_char(i); - i = utf8_next_char(i); + i++; + // utf8_next_char() assumes NUL-terminated data but callers pass + // length-delimited buffers, so bound the continuation-byte scan. + while (i < end && UTF8_IS_CONT(*i)) { + i++; + } if (self->in_osc_command) { - if (c == 0x1b && i[0] == '\\') { + if (c == 0x1b && i < end && i[0] == '\\') { self->in_osc_command = false; self->status_x = 0; self->status_y = 0; @@ -245,6 +251,10 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con uint8_t n_args = 1; #endif for (; j < 6; j++) { + if (i + j >= end) { + c = 0; + break; + } if ('0' <= i[j] && i[j] <= '9') { vt_args[0] = vt_args[0] * 10 + (i[j] - '0'); } else { @@ -252,10 +262,14 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con break; } } - if (i[0] == '[') { + if (i < end && i[0] == '[') { for (uint8_t i_args = 1; i_args < 3 && c == ';'; i_args++) { vt_args[i_args] = 0; for (++j; j < 12; j++) { + if (i + j >= end) { + c = 0; + break; + } if ('0' <= i[j] && i[j] <= '9') { vt_args[i_args] = vt_args[i_args] * 10 + (i[j] - '0'); #if CIRCUITPY_TERMINALIO_VT100 @@ -267,7 +281,7 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con } } } - if (c == '?') { + if (c == '?' && i + 4 < end) { #if CIRCUITPY_TERMINALIO_VT100 if (i[2] == '2' && i[3] == '5') { // cursor visibility commands @@ -358,7 +372,7 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con i += j + 1; } #if CIRCUITPY_TERMINALIO_VT100 - } else if (i[0] == 'M') { + } else if (i < end && i[0] == 'M') { if (self->cursor_y != SCRNMOD(self->vt_scroll_top)) { if (self->cursor_y > 0) { self->cursor_y = self->cursor_y - 1; @@ -400,11 +414,11 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con } start_y = self->cursor_y; i++; - } else if (i[0] == 'D') { + } else if (i < end && i[0] == 'D') { self->cursor_y++; i++; #endif - } else if (i[0] == ']' && c == ';') { + } else if (i < end && i[0] == ']' && c == ';') { self->in_osc_command = true; self->osc_command = vt_args[0]; i += j + 1; @@ -450,6 +464,9 @@ size_t common_hal_terminalio_terminal_write(terminalio_terminal_obj_t *self, con start_y = self->cursor_y; } } + if (i > end) { + i = end; + } return i - data; } diff --git a/shared/runtime/stdout_helpers.c b/shared/runtime/stdout_helpers.c index 5d68747b6aa..0158ae7fe1d 100644 --- a/shared/runtime/stdout_helpers.c +++ b/shared/runtime/stdout_helpers.c @@ -50,7 +50,8 @@ void mp_hal_stdout_tx_strn_cooked(const char *str, size_t len) { i = 1; } // Lump all characters on the next line together. - while ((last_cr || str[i] != '\n') && i < len) { + // Check i < len first so we never read str[len]. + while (i < len && (last_cr || str[i] != '\n')) { last_cr = str[i] == '\r'; i++; } From fababfaccb43aa9b527ce6d95a0e3c058b360129 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Wed, 9 Sep 2026 12:40:01 -0700 Subject: [PATCH 2/2] Fix build on older compilers --- .../native/native_sim_asan/autogen_board_info.toml | 2 +- ports/zephyr-cp/zephyr_main.c | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml b/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml index 6ce71ece57f..e738a839210 100644 --- a/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml +++ b/ports/zephyr-cp/boards/native/native_sim_asan/autogen_board_info.toml @@ -32,7 +32,7 @@ bitbangio = false bitmapfilter = true # Zephyr board has busio bitmaptools = true # Zephyr board has busio bitops = false -board = false +board = true # Always enabled busdisplay = true # Zephyr board has busio busio = true # Zephyr board has busio camera = false diff --git a/ports/zephyr-cp/zephyr_main.c b/ports/zephyr-cp/zephyr_main.c index 68afea5e912..bfa1a4df0da 100644 --- a/ports/zephyr-cp/zephyr_main.c +++ b/ports/zephyr-cp/zephyr_main.c @@ -2,7 +2,14 @@ extern int circuitpython_main(void); -#if defined(__SANITIZE_ADDRESS__) || (defined(__has_feature) && __has_feature(address_sanitizer)) +// Note: __has_feature must only be evaluated in a nested #if guarded by +// defined(__has_feature); older GCC versions fail to parse it otherwise. +#if defined(__has_feature) +#if __has_feature(address_sanitizer) +#define CP_HAS_ASAN 1 +#endif +#endif +#if defined(__SANITIZE_ADDRESS__) || defined(CP_HAS_ASAN) // ASAN's stack-use-after-return detection (on by default in recent runtimes) // moves C locals into "fake stack" frames on the heap. The GC scans the real // machine stack for live object pointers, so with fake stacks enabled it misses