Skip to content

Commit 9883d8e

Browse files
committed
py/persistentcode: Maintain root ptr list of imported native .mpy code.
On ports where normal heap memory can contain executable code (eg ARM-based ports such as stm32), native code loaded from an .mpy file may be reclaimed by the GC because there's no reference to the very start of the native machine code block that is reachable from root pointers (only pointers to internal parts of the machine code block are reachable, but that doesn't help the GC find the memory). This commit fixes this issue by maintaining an explicit list of root pointers pointing to native code that is loaded from an .mpy file. This is not needed for all ports so is selectable by the new configuration option MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE. It's enabled by default if a port does not specify any special functions to allocate or commit executable memory. A test is included to test that native code loaded from an .mpy file does not get reclaimed by the GC. Fixes micropython#6045. Signed-off-by: Damien George <[email protected]>
1 parent 8da40ba commit 9883d8e

File tree

6 files changed

+126
-1
lines changed

6 files changed

+126
-1
lines changed

py/mpconfig.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,18 @@
351351
// Convenience definition for whether any native or inline assembler emitter is enabled
352352
#define MICROPY_EMIT_MACHINE_CODE (MICROPY_EMIT_NATIVE || MICROPY_EMIT_INLINE_ASM)
353353

354+
// Whether native relocatable code loaded from .mpy files is explicitly tracked
355+
// so that the GC cannot reclaim it. Needed on architectures that allocate
356+
// executable memory on the MicroPython heap and don't explicitly track this
357+
// data some other way.
358+
#ifndef MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
359+
#if !MICROPY_EMIT_MACHINE_CODE || defined(MP_PLAT_ALLOC_EXEC) || defined(MP_PLAT_COMMIT_EXEC)
360+
#define MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE (0)
361+
#else
362+
#define MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE (1)
363+
#endif
364+
#endif
365+
354366
/*****************************************************************************/
355367
/* Compiler configuration */
356368

py/mpstate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ typedef struct _mp_state_vm_t {
167167
mp_obj_dict_t *mp_module_builtins_override_dict;
168168
#endif
169169

170+
#if MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
171+
// An mp_obj_list_t that tracks relocated native code to prevent the GC from reclaiming them.
172+
mp_obj_t track_reloc_code_list;
173+
#endif
174+
170175
// include any root pointers defined by a port
171176
MICROPY_PORT_ROOT_POINTERS
172177

py/persistentcode.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* The MIT License (MIT)
55
*
6-
* Copyright (c) 2013-2016 Damien P. George
6+
* Copyright (c) 2013-2020 Damien P. George
77
*
88
* Permission is hereby granted, free of charge, to any person obtaining a copy
99
* of this software and associated documentation files (the "Software"), to deal
@@ -516,6 +516,18 @@ STATIC mp_raw_code_t *load_raw_code(mp_reader_t *reader, qstr_window_t *qw) {
516516
fun_data = MP_PLAT_COMMIT_EXEC(fun_data, fun_data_len, opt_ri);
517517
#else
518518
if (prelude.scope_flags & MP_SCOPE_FLAG_VIPERRELOC) {
519+
#if MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
520+
// If native code needs relocations then it's not guaranteed that a pointer to
521+
// the head of `buf` (containing the machine code) will be retained for the GC
522+
// to trace. This is because native functions can start inside `buf` and so
523+
// it's possible that the only GC-reachable pointers are pointers inside `buf`.
524+
// So put this `buf` on a list of reachable root pointers.
525+
if (MP_STATE_PORT(track_reloc_code_list) == MP_OBJ_NULL) {
526+
MP_STATE_PORT(track_reloc_code_list) = mp_obj_new_list(0, NULL);
527+
}
528+
mp_obj_list_append(MP_STATE_PORT(track_reloc_code_list), MP_OBJ_FROM_PTR(fun_data));
529+
#endif
530+
// Do the relocations.
519531
mp_native_relocate(&ri, fun_data, (uintptr_t)fun_data);
520532
}
521533
#endif

py/runtime.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ void mp_init(void) {
106106
MP_STATE_VM(mp_module_builtins_override_dict) = NULL;
107107
#endif
108108

109+
#if MICROPY_PERSISTENT_CODE_TRACK_RELOC_CODE
110+
MP_STATE_VM(track_reloc_code_list) = MP_OBJ_NULL;
111+
#endif
112+
109113
#if MICROPY_PY_OS_DUPTERM
110114
for (size_t i = 0; i < MICROPY_PY_OS_DUPTERM; ++i) {
111115
MP_STATE_VM(dupterm_objs[i]) = MP_OBJ_NULL;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Test that native code loaded from a .mpy file is retained after a GC.
2+
3+
try:
4+
import gc, sys, uio, uos
5+
6+
sys.implementation.mpy
7+
uio.IOBase
8+
uos.mount
9+
except (ImportError, AttributeError):
10+
print("SKIP")
11+
raise SystemExit
12+
13+
14+
class UserFile(uio.IOBase):
15+
def __init__(self, data):
16+
self.data = memoryview(data)
17+
self.pos = 0
18+
19+
def readinto(self, buf):
20+
n = min(len(buf), len(self.data) - self.pos)
21+
buf[:n] = self.data[self.pos : self.pos + n]
22+
self.pos += n
23+
return n
24+
25+
def ioctl(self, req, arg):
26+
return 0
27+
28+
29+
class UserFS:
30+
def __init__(self, files):
31+
self.files = files
32+
33+
def mount(self, readonly, mksfs):
34+
pass
35+
36+
def umount(self):
37+
pass
38+
39+
def stat(self, path):
40+
if path in self.files:
41+
return (32768, 0, 0, 0, 0, 0, 0, 0, 0, 0)
42+
raise OSError
43+
44+
def open(self, path, mode):
45+
return UserFile(self.files[path])
46+
47+
48+
# Pre-compiled examples/natmod/features0 example for various architectures, keyed
49+
# by the required value of sys.implementation.mpy.
50+
features0_file_contents = {
51+
# -march=x64 -mcache-lookup-bc
52+
0xB05: b'M\x05\x0b\x1f \x84b\xe9/\x00\x00\x00SH\x8b\x1ds\x00\x00\x00\xbe\x02\x00\x00\x00\xffS\x18\xbf\x01\x00\x00\x00H\x85\xc0u\x0cH\x8bC \xbe\x02\x00\x00\x00[\xff\xe0H\x0f\xaf\xf8H\xff\xc8\xeb\xe6ATUSH\x8b\x1dA\x00\x00\x00H\x8b\x7f\x08L\x8bc(A\xff\xd4H\x8d5\x1f\x00\x00\x00H\x89\xc5H\x8b\x05-\x00\x00\x00\x0f\xb78\xffShH\x89\xefA\xff\xd4H\x8b\x03[]A\\\xc3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x84@\x12factorial\x10\x00\x00\r \x01"\x9f\x1c\x01\x1e\xff',
53+
# -march=armv7m
54+
0x1605: b"M\x05\x16\x1f \x84\x12\x1a\xe0\x00\x00\x13\xb5\nK\nJ{D\x9cX\x02!\xe3h\x98G\x03F\x01 3\xb9\x02!#i\x01\x93\x02\xb0\xbd\xe8\x10@\x18GXC\x01;\xf4\xe7\x00\xbfj\x00\x00\x00\x00\x00\x00\x00\xf8\xb5\tN\tK~D\xf4X@hgi\xb8G\x05F\x07K\x07I\xf2XyD\x10\x88ck\x98G(F\xb8G h\xf8\xbd6\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x01\x84\x00\x12factorial\x10\x00\x00\r<\x01>\x9f8\x01:\xff",
55+
}
56+
57+
# Populate other armv7m-derived archs based on armv7m.
58+
for arch in (0x1A05, 0x1E05, 0x2205):
59+
features0_file_contents[arch] = features0_file_contents[0x1605]
60+
61+
if sys.implementation.mpy not in features0_file_contents:
62+
print("SKIP")
63+
raise SystemExit
64+
65+
# These are the test .mpy files.
66+
user_files = {"/features0.mpy": features0_file_contents[sys.implementation.mpy]}
67+
68+
# Create and mount a user filesystem.
69+
uos.mount(UserFS(user_files), "/userfs")
70+
sys.path.append("/userfs")
71+
72+
# Import the native function.
73+
gc.collect()
74+
from features0 import factorial
75+
76+
# Free the module that contained the function.
77+
del sys.modules["features0"]
78+
79+
# Run a GC cycle which should reclaim the module but not the function.
80+
gc.collect()
81+
82+
# Allocate lots of fragmented memory to overwrite anything that was just freed by the GC.
83+
for i in range(1000):
84+
[]
85+
86+
# Run the native function, it should not have been freed or overwritten.
87+
print(factorial(10))
88+
89+
# Unmount and undo path addition.
90+
uos.umount("/userfs")
91+
sys.path.pop()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3628800

0 commit comments

Comments
 (0)