Skip to content

fix(modem): UartTerminal::on_read usage/set data race - #1112

Closed
markxoe wants to merge 1 commit into
espressif:masterfrom
markxoe:fix/esp-modem-on_read-datarace
Closed

fix(modem): UartTerminal::on_read usage/set data race#1112
markxoe wants to merge 1 commit into
espressif:masterfrom
markxoe:fix/esp-modem-on_read-datarace

Conversation

@markxoe

@markxoe markxoe commented Jul 26, 2026

Copy link
Copy Markdown

Description

While testing esp_modem (v2.0.2) with a SIM7080 and ESP32-S3 I reproducibly got several crashes while calling esp_modem_set_mode. The stack traces showed either double-free or segfault. Further debugging with CONFIG_HEAP_POISONING_COMPREHENSIVE narrowed the problem down to this on_read() callback being invoked even after it had already been set to nullptr.
The problem seems to originate from a data race where set_read_cb is called between the check before usage of on_read and the call of on_read.

This PR fixes this issue by locking a mutex when on_read is either set or used.

Related

A quick search found #1014 to be related: this bug is marked as UART-003 but not fixed in the PR. I found no further mention of this bug.

Testing

Only manual testing pre- and post-fixing were done due to the bug being complex to test. The bug has not yet occurred after the fix and thorough manual testing.


Checklist

Before submitting a Pull Request, please ensure the following:

  • 🚨 This PR does not introduce breaking changes.
  • All CI checks (GH Actions) pass.
  • Documentation is updated as needed.
  • Tests are updated or added as necessary.
  • Code is well-commented, especially in complex areas.
  • Git history is clean — commits are squashed to the minimum necessary.

Note

Medium Risk
Touches concurrency on the UART RX hot path; incorrect locking could deadlock or stall modem I/O, but the change is narrowly scoped and matches existing esp_modem Lock primitives.

Overview
Fixes crashes during esp_modem_set_mode caused by a race: the UART task could invoke on_read after another thread cleared it via set_read_cb.

UartTerminal now holds a recursive Lock (on_read_lock) declared before the UART task so it outlives the task. set_read_cb takes the lock while swapping the callback; the UART task loop takes the same lock around the null-check and invocation of on_read on both the UART_DATA path and the polling timeout path. Recursion is required because on_read may call set_read_cb re-entrantly.

Reviewed by Cursor Bugbot for commit cebfab0. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant

CLAassistant commented Jul 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread components/esp_modem/src/esp_modem_uart.cpp
@espressif-bot espressif-bot added the Status: Opened Issue is new label Jul 26, 2026
UartTerminal::set_read_cb can be called between the check and call of on_read in UartTerminal::task triggering a data race between them resulting in a segfault. This commit fixes this by adding a lock for the use of on_read
@markxoe
markxoe force-pushed the fix/esp-modem-on_read-datarace branch from 3b81768 to cebfab0 Compare July 26, 2026 16:32

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cebfab0. Configure here.

Scoped<Lock> lock(on_read_lock);
if (len && on_read) {
on_read(nullptr, len);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Callback destroyed during nested set

Medium Severity

The new recursive on_read_lock is documented to allow on_read to call set_read_cb and replace the callback while it runs, but the task still invokes on_read directly. A nested set_read_cb assigns over that same std::function, destroying the callable mid-invocation. That path is used by DTE::on_read when a reply completes, and can cause heap corruption or crashes.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cebfab0. Configure here.

@david-cermak

Copy link
Copy Markdown
Collaborator

Hi @markxoe

Thank you for the contribution with your clean and minimal patch to modem sources!
Yes, this bug was found and never addressed. We already had a contribution though, fixing this (and some other related bugs UART-004, UART-005, CMUX teardown, etc...) in #1083.
Hasn't been merged yet (due to some formal issues + the fact that its merge-base's on release-v1.4), but i'd still prefer the 1083 patch version -- tried to quickly cherry-pick onto master in #1113.

@markxoe

markxoe commented Aug 2, 2026

Copy link
Copy Markdown
Author

Hi @david-cermak that looks like the better solution, thanks for merging it so quickly!

@markxoe markxoe closed this Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Opened Issue is new

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants