Skip to content

ports/embed: segfault in mp_compile() #11781

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

Closed
cwalther opened this issue Jun 14, 2023 · 2 comments · Fixed by #13653
Closed

ports/embed: segfault in mp_compile() #11781

cwalther opened this issue Jun 14, 2023 · 2 comments · Fixed by #13653
Labels

Comments

@cwalther
Copy link
Contributor

There is a bug in the embed port that can cause segfaults while compiling Python code in memory-constrained situations. I have not managed to construct a simple reproducer yet, since it depends on memory allocation patterns, but the root cause is easy to verify, so I’m reporting it already.

mp_stack_ctrl_init(), called two levels deep in main()mp_embed_init()mp_stack_ctrl_init(), captures a too low stack_top, so that the local variable parse_tree of mp_embed_exec_str(), called only one level deep in main()mp_embed_exec_str(), ends up outside of the stack range looked at by the garbage collector in gc_helper_collect_regs_and_stack(). When garbage collection is invoked at just the wrong moment during compilation, the blocks pointed at by the contents of parse_tree are therefore freed prematurely, and when they are later reused and overwritten, that corrupts the pointers inside them, which is likely to lead to memory access failures in mp_parse_tree_clear() at the latest.

This is easily verified with

diff --git a/ports/embed/port/embed_util.c b/ports/embed/port/embed_util.c
index 14f508977..816e28a36 100644
--- a/ports/embed/port/embed_util.c
+++ b/ports/embed/port/embed_util.c
@@ -49,6 +49,7 @@ void mp_embed_exec_str(const char *src) {
         mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
         qstr source_name = lex->source_name;
         mp_parse_tree_t parse_tree = mp_parse(lex, MP_PARSE_FILE_INPUT);
+        assert((char*)&parse_tree <= MP_STATE_THREAD(stack_top));
         mp_obj_t module_fun = mp_compile(&parse_tree, source_name, true);
         mp_call_function_0(module_fun);
         nlr_pop();

This assert fails for me on macOS 13 x86_64, compiling with clang 14.

I am not sure how to best fix this. Ideas so far:

  • Remove mp_stack_ctrl_init() from mp_embed_init() and require the application itself to call it, at the same or higher level than it will later call mp_embed_exec_str().
  • Have mp_stack_ctrl_init() apply some offset to the detected stack top to account for its own call depth. I have no idea how to generically and portably determine what that offset should be, though. (Underestimating will fail to fix the problem, overestimating might run into unmapped memory above the stack.)
  • mp_embed_exec_str() could check whether parse_tree is above stack_top and if so adjust the latter.

Any opinions?

@cwalther cwalther added the bug label Jun 14, 2023
@stinos
Copy link
Contributor

stinos commented Jun 15, 2023

This might precisely be the reason that the unix port does mp_stack_ctrl_init as the very first thing.

Remove mp_stack_ctrl_init() from mp_embed_init() and require the application itself to call it

Probably the way to go, though should be configurable perhaps, and in any case an error/exit should be generated immediately if someone forgets to call it since otherwise the resulting errors might be quite obscure.

FWIW I've been using a function similar to mp_embed_init for years and never had stack issues. Actually, also in light of #11430, looking at the current state where there's a myriad of variations on init/exec_str/exec_mpy/deinit/... spread over ports and utility functions I think it would be good to try and unify this into common functions which are either configurable at compiletime or else at runtime. Also by means of documentation. Take mp_embed_exec_str for example: it's there, it's an example, but once you actually want to use it it's going to take a couple of minutes and then you realize you need the return value and you're going to turn to one of th 5 or 10 other implementations which are all tiny variations on the same thing. Next you want to use MICROPYPATH and have to copy that from the unix port (or so), and so on.

cwalther added a commit to cwalther/circuitpython that referenced this issue Jun 17, 2023
cwalther added a commit to cwalther/circuitpython that referenced this issue Jun 17, 2023
@cwalther
Copy link
Contributor Author

I have a reproducer: cwalther@b404d15. It’s not a segfault but an assert this time (apparently the thing the pointer is overwritten with isn’t that outlandish, but still sufficiently wrong), but that should be enough to illustrate the problem. The added printf()s are not important, they were just useful to nudge the garbage collections into the right spot.

Build the embedding example in the usual way with make -f micropython_embed.mk && make and run it.

Actual result:

cwalther@tartan embedding % ./embed                                         
PARSE
COMPILE
RUN
hello world! [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]eol
DONE
PARSE
COMPILE
COLLECT
Assertion failed: (ATB_GET_KIND(area, block) == AT_HEAD), function gc_free, file gc.c, line 776.
zsh: abort      ./embed

Expected result:

cwalther@tartan embedding % ./embed                                         
PARSE
COMPILE
RUN
hello world! [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]eol
DONE
PARSE
COMPILE
COLLECT
RUN
iter 00000000
iter 00000001
iter 00000002
iter 00000003
iter 00000004
iter 00000005
iter 00000006
iter 00000007
iter 00000008
iter 00000009
caught exception ZeroDivisionError('divide by zero',)
run GC collect
COLLECT
hello world! f 1
hello world! f 2
hello world! f 3
hello world! f 4
hello world! f 5
hello world! f 6
hello world! f 7
hello world! f 8
hello world! f 9
hello world! f 10
hello world! f 11
hello world! f 12
hello world! f 13
hello world! f 14
hello world! f 15
hello world! f 16
hello world! f 17
hello world! f 18
hello world! f 19
hello world! f 20
finish
DONE

The expected result can be achieved by moving the call of mp_stack_ctrl_init() as proposed: cwalther@60a889e

yamt added a commit to yamt/micropython that referenced this issue Feb 13, 2024
Fixes: micropython#11781

Signed-off-by: YAMAMOTO Takashi <yamamoto@midokura.com>
yamt added a commit to yamt/micropython that referenced this issue Feb 13, 2024
Fixes: micropython#11781

Signed-off-by: YAMAMOTO Takashi <yamamoto@midokura.com>
dpgeorge pushed a commit to yamt/micropython that referenced this issue Feb 14, 2024
Obtaining the stack-top via a few function calls may yield a pointer which
is too deep within the stack.  So require the user to obtain it from a
higher level (or via some other means).

Fixes issue micropython#11781.

Signed-off-by: YAMAMOTO Takashi <yamamoto@midokura.com>
tytgatlieven pushed a commit to tytgatlieven/micropython-esp32c6 that referenced this issue Mar 19, 2024
Obtaining the stack-top via a few function calls may yield a pointer which
is too deep within the stack.  So require the user to obtain it from a
higher level (or via some other means).

Fixes issue micropython#11781.

Signed-off-by: YAMAMOTO Takashi <yamamoto@midokura.com>
intx82 pushed a commit to intx82/micropython_a9_gprs that referenced this issue Apr 21, 2024
Obtaining the stack-top via a few function calls may yield a pointer which
is too deep within the stack.  So require the user to obtain it from a
higher level (or via some other means).

Fixes issue micropython#11781.

Signed-off-by: YAMAMOTO Takashi <yamamoto@midokura.com>
graeme-winter pushed a commit to winter-special-projects/micropython that referenced this issue Sep 21, 2024
Obtaining the stack-top via a few function calls may yield a pointer which
is too deep within the stack.  So require the user to obtain it from a
higher level (or via some other means).

Fixes issue micropython#11781.

Signed-off-by: YAMAMOTO Takashi <yamamoto@midokura.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants