Found during jpegio Phase 2 (2026-09-03) on a unix build containing only lvgl-bindings aa6c6bc + lvgl-micropython d7ea193 (no displayif, so not jpegio's). Reproduces on MicroPython v1.28.0 unix, stock upstream, no cmods patches.
Repro (repro.py; pass cycle to trigger):
import sys, gc
import lvgl as lv
W, H = 320, 240
def setup():
disp = lv.display_create(W, H)
disp.set_color_format(lv.COLOR_FORMAT.RGB565)
buf = lv.draw_buf_create(W, H, lv.COLOR_FORMAT.RGB565, 0)
disp.set_draw_buffers(buf, None)
disp.set_render_mode(lv.DISPLAY_RENDER_MODE.PARTIAL)
disp.set_flush_cb(lambda d, a, c: d.flush_ready())
return disp, buf
lv.init()
disp, buf = setup()
if "cycle" in sys.argv:
print("cycle"); lv.deinit(); lv.init(); disp, buf = setup()
lbl = lv.label(lv.screen_active()); lbl.set_text("hello")
for i in range(3):
lbl.invalidate(); lv.refr_now(disp); print("refresh", i, "ok")
print("collect..."); gc.collect(); print("collect ok", gc.mem_free())
Without cycle: passes. With cycle: the three refreshes print, then gc.collect() dies with SIGBUS.
Backtrace (unstripped build, STRIP=):
mp_load_method_maybe (obj=0x7ffff7a08560, attr=__del__) py/runtime.c:1201 ← gc_sweep_run_finalisers py/gc.c:639 ← gc_collect_end ← gc_collect ← py_gc_collect.
So the sweep's finaliser walk reaches an object whose type pointer is no longer valid after the first lv.deinit() freed the LVGL side: something the binding still holds (or that the GC still sees as having a finaliser) from the first lv.init() lifetime survives the cycle. Suspects: the display/draw-buffer wrappers from the first setup() (still referenced by Python until rebound) whose C objects lv.deinit() freed, or the callback registry (set_flush_cb closure) retained across the cycle — see the widget-retention lesson from the v9.5.22 callback fixes.
Not blocking anything: displayif's LVGL test orders its deinit/init section last and avoids gc.collect() after it for this reason (displayif tests/jpegio/test_jpegio_lvgl.py, section 7). Worth fixing because lv.deinit()/lv.init() is the documented way to reset LVGL in a long-running REPL session.
Found during jpegio Phase 2 (2026-09-03) on a unix build containing only lvgl-bindings aa6c6bc + lvgl-micropython d7ea193 (no displayif, so not jpegio's). Reproduces on MicroPython v1.28.0 unix, stock upstream, no cmods patches.
Repro (
repro.py; passcycleto trigger):Without
cycle: passes. Withcycle: the three refreshes print, thengc.collect()dies with SIGBUS.Backtrace (unstripped build,
STRIP=):mp_load_method_maybe (obj=0x7ffff7a08560, attr=__del__) py/runtime.c:1201←gc_sweep_run_finalisers py/gc.c:639←gc_collect_end←gc_collect←py_gc_collect.So the sweep's finaliser walk reaches an object whose type pointer is no longer valid after the first
lv.deinit()freed the LVGL side: something the binding still holds (or that the GC still sees as having a finaliser) from the firstlv.init()lifetime survives the cycle. Suspects: the display/draw-buffer wrappers from the firstsetup()(still referenced by Python until rebound) whose C objectslv.deinit()freed, or the callback registry (set_flush_cbclosure) retained across the cycle — see the widget-retention lesson from the v9.5.22 callback fixes.Not blocking anything: displayif's LVGL test orders its deinit/init section last and avoids
gc.collect()after it for this reason (displayiftests/jpegio/test_jpegio_lvgl.py, section 7). Worth fixing becauselv.deinit()/lv.init()is the documented way to reset LVGL in a long-running REPL session.