Skip to content

Guard rebuildForLocation against synchronous re-entry via the poll pump - #2

Merged
selmapi merged 1 commit into
mainfrom
fix/map-rebuild-reentrancy-guard
Jul 8, 2026
Merged

Guard rebuildForLocation against synchronous re-entry via the poll pump#2
selmapi merged 1 commit into
mainfrom
fix/map-rebuild-reentrancy-guard

Conversation

@selmapi

@selmapi selmapi commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem

services::map::rebuildForLocation() blocks on HTTP while pumping the injected poll fn (wifiLoop → WiFiManager process()), which synchronously dispatches web handlers mid-rebuild — including POST /api/map/rebuild itself — nesting rebuildForLocation inside itself. Pre-existing; flagged by the Opus review of PR #1 (2026-07-06). Worst case was a redundant fetch + tmp-file churn (release/init are idempotent), but it deserved a guard.

Fix

  • New Arduino-free RAII RebuildLock in include/services/rebuild_guard.h (natively testable, same idiom as ui/color_blend.h). A failed nested acquisition is a no-op on destruction — the outer lock alone clears the flag, on every return path.
  • New RebuildResult::kBusy; nested/concurrent calls return it immediately.
  • handleMapRebuild maps kBusy to the existing {"ok":false,"building":true,...} shape, so the webapp's auto-poll retries with zero webapp changes.
  • 4 new native Unity tests, including the load-bearing one: a failed nested lock's destructor must not clear the busy flag.

Verification

  • pio test -e native: 58/58 (54 baseline + 4 new), run independently by orchestrator and implementer.
  • pio run -e supermini: SUCCESS (RAM 17.6%, Flash 42.3%).
  • Independent Opus review: APPROVE, no blocking findings — verified no return path can leave the flag stuck, no other RebuildResult branch mishandles kBusy, and the webapp poll loop terminates (bounded retries).

🤖 Generated with Claude Code

rebuildForLocation() holds a blocking HTTP fetch open while pumping
s_poll_fn (wifiLoop -> WiFiManager::process()) to keep the portal alive.
That poll synchronously dispatches web handlers, including
POST /api/map/rebuild (handleMapRebuild), so a second rebuild request
arriving mid-fetch can nest a second rebuildForLocation call inside the
first. Worst case today is a redundant fetch and tmp-file churn (release/
init are idempotent), but there was no guard against it. Flagged by Opus
review 2026-07-06 as a pre-existing, unguarded re-entrancy hole.

Adds services::map::RebuildLock (include/services/rebuild_guard.h), a
tiny Arduino-free RAII non-recursive busy flag, same idiom as
ui/color_blend.h, so it's natively testable. Only the lock instance that
actually acquired the flag clears it on destruction -- a failed nested
lock is a no-op on destruct, which is what makes the outer call the sole
owner of the flag across all of rebuildForLocation's ~10 return paths.

rebuildForLocation() now takes the lock first thing and returns the new
RebuildResult::kBusy if already held. web_app.cpp's handleMapRebuild
treats kBusy exactly like kBuilding in the JSON response
({"ok":false,"building":true,...}), so the webapp's existing auto-poll
handles it with zero webapp changes.

New native test (test/native/test_rebuild_guard) covers: first lock
acquires; nested lock fails while held; a failed nested lock's
destruction does not clear the flag (the regression this design
prevents); and the outer lock's destruction clears the flag for a
fresh acquire. Header-only, no build_src_filter change needed.
@selmapi
selmapi merged commit 28dcccc into main Jul 8, 2026
@selmapi
selmapi deleted the fix/map-rebuild-reentrancy-guard branch July 8, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant