Fix LVGL thread race between the main thread and the DisplayApp task#198
Open
faisal-shah wants to merge 2 commits into
Open
Fix LVGL thread race between the main thread and the DisplayApp task#198faisal-shah wants to merge 2 commits into
faisal-shah wants to merge 2 commits into
Conversation
LVGL is not thread-safe, but the simulator drives it from two threads: the DisplayApp task, and the SDL main thread whose refresh_screen() creates/deletes the "Screen is OFF" overlay and runs lv_task_handler while the display task sleeps. The wake/sleep handoff between them corrupts the LVGL heap. AddressSanitizer shows a heap-use-after-free: main-thread lv_obj_del(screen_off_bg) racing the display task's lv_task_handler (alloc/free in refresh_screen on T0, use in _lv_disp_refr_task on the displayapp thread). A wake/sleep stress loop (right-click toggle, ~600ms period) crashes an ASan build within ~4 cycles. This adds sim/displayapp/LvglGuard.h, a recursive mutex behind an LVGL_GUARD() macro, and takes it in refresh_screen(). The header is placed so it shadows a matching no-op header in InfiniTime, whose DisplayApp::Refresh takes the same guard around its two LVGL regions (companion change, see PR description); on hardware the macro stays a no-op. With both sides guarded, 300 stress cycles run clean under ASan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeA4vFDz8WACLogQhg53Kd
The main loop also pumps SDL input, so parking it on a lock held by the DisplayApp task would turn any stall inside that task into a whole-process wedge (input dead) rather than a frozen watch screen. Observed once during long-running use: the process was alive with the main thread in futex_wait. The main thread now try-locks and skips the overlay frame if the display task holds the lock (it runs again ~30 ms later), so it can never be part of a deadlock cycle. Mutual exclusion is unchanged: LVGL is still only touched under the lock, and the 300-cycle ASan stress stays clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeA4vFDz8WACLogQhg53Kd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The complete fix needs a small companion change in InfiniTime: InfiniTimeOrg/InfiniTime#2447.
The bug
LVGL is not thread-safe, but InfiniSim drives it from two threads:
lv_task_handler, screen loads), andrefresh_screen()creates and deletes the "Screen is OFF" overlay objects and runslv_task_handlerwhile the display task sleeps.The wake/sleep handoff between the two corrupts the LVGL heap. It reproduces easily. Toggle sleep/wake with the right mouse button every ~600 ms, and an AddressSanitizer build crashes within about 4 cycles:
In a normal build the same corruption surfaces later as a SIGSEGV in the style walk during refresh (
_lv_style_get_intreading a freed style map). The existingscreen_off_delete_cbdouble-free guard covers one symptom but not the underlying race.The fix
A
std::recursive_mutexbehindLVGL_GUARD()(sim/displayapp/LvglGuard.h), with the two sides locking differently on purpose:try_lock(LVGL_TRY_GUARD()) and skips the overlay frame if the display task holds it. The main loop also pumps SDL input, so it must never park on a lock held by another thread. Doing so would turn any stall inside the display task into a whole-process wedge, dead input, rather than a frozen watch screen. A skipped overlay frame is harmless; the loop runs again about 30 ms later. I hit exactly this while testing a blocking first version: the process stayed alive with the main thread infutex_wait.The other half of the race is the DisplayApp task itself, which is InfiniTime code. InfiniTimeOrg/InfiniTime#2447 adds a no-op
displayapp/LvglGuard.hto InfiniTime and takesLVGL_GUARD()around the two LVGL regions ofDisplayApp::Refresh, not across the blocking queue wait. On hardware the macro compiles to nothing, since LVGL has a single owner there. In the sim, this repo's header shadows it via include-path order (the same mechanism the sim already uses fornrf_log.h), so both threads serialize.Without the InfiniTime side, this change alone is safe but only partial: it serializes the overlay operations against the sim's own off-screen
lv_task_handler, not against the display task.Verification
cmake && make), no new warnings.