Skip to content

F-21: Use-after-free when returning to the main screen #24

Open

Description

@bazauto

Severity: HIGH · Effort: Medium · Batch: 1 · Status: Code done in #21 (7c980f9), awaiting bench verification

Found in the second full review of the firmware. Recorded with the rest of the pass (F-19…F-44) in docs/REVIEW_REMEDIATION_PLAN_2.md, which is where its status is kept.

Description

ThrottleController::updateUI reads the UI callback and its userData without a lock, and
MainScreen::onUIUpdateNeeded then blocks in lvgl_port_lock(200). If the LVGL task is running
a Back handler at that moment, AppController::showMainScreen() destroys the old MainScreen
and builds a new one while holding the LVGL lock. Deregistering in the destructor is too late:
the encoder or network task already holds the old pointer, and when the lock frees it repaints
freed memory — heap corruption or a reboot with trains running. PowerStatusBar's track-power
callback has the same shape.

Plan

Stop destroying the main screen. AppController builds one MainScreen for the life of the
application and showMainScreen() re-shows it (lv_scr_load plus a repaint). A screen that is
never freed cannot be used after it is freed, and the fix also closes F-32.

That makes MainScreen::create a one-off, which exposes a second problem: it overwrote the
WiThrottle client's single connection-state slot, which the active backend owns and the knob
gating depends on, and JmriConfigScreen then nulled that slot on Back. Once the main screen is
no longer rebuilt on return, nothing would put it back. So:

  1. MainScreen no longer registers on WiThrottleClient at all: the backend's own callback
    already reaches updateUI, which repaints the main screen.
  2. JmriConfigScreen polls its status on an LVGL timer, as SettingsScreen and
    OrchestratorConfigScreen already do, instead of taking either client's slot.

Files: AppController.h/.cpp, MainScreen.h/.cpp, JmriConfigScreen.h/.cpp

Acceptance Criteria

  • MainScreen is constructed once per boot; MainScreen::create now takes only the
    controller.
  • No UI class calls setConnectionStateCallback on a client.
  • Knob gating still follows the WiThrottle link after visiting the JMRI screen and
    returning.
  • Bench: navigate Settings → Back twenty times while spinning a knob, on a build with
    CONFIG_HEAP_POISONING_COMPREHENSIVE; no crash, no heap-corruption report.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions