Skip to content

[Security] fetch_history_events tight-loops without sleep when from_height exceeds indexer finalized height (CPU spin until restart) #468

Description

@byfor8

Summary

fetch_history_events tight-loops without sleep or break when from_height exceeds the indexer's finalized height. The branch logs "will finish" then continues an infinite loop, re-calling get_sync_block_height as fast as the RPC client allows. Other empty/error tip paths sleep 500ms; this one spins. The spawned task has no cancellation token, so the spin persists until process restart.

Security impact: local resource exhaustion (CPU spin) on the event-watch task, stalling catchup progress for that contract. No fund theft path.

Affected component

  • Repository: GOATNetwork/bitvm-node
  • Commit reviewed: 1ff9a4aeced45309a097eeba5955e26164302ab3 (current HEAD at time of review)
  • Files: node/src/scheduled_tasks/event_watch_task.rs
  • Component class: Implementation (event watcher, Gateway catchup)

Description

In fetch_history_events (node/src/scheduled_tasks/event_watch_task.rs, event_watch_task.rs:1120-1125 at the pin):

            if watch_contract.from_height > current_finalized {
                info!(
                    "Contract {task_name} fetch history events will finish, as current finalize height: {current_finalized} is litter than watch from height: {}",
                    watch_contract.from_height,
                );
                continue;
            }

The continue re-enters the loop, which immediately re-calls get_sync_block_height. Compare the sibling branches in the same loop: the empty-tip path (Ok(None)) and the error path both sleep(Duration::from_millis(500)).await; before continue. Only this arm spins unbounded.

Reachability: the condition becomes true when the indexer tip regresses below an already-advanced from_height while the loop is running (subgraph reindex/reset, replica lag) — the cursor only advances via to_height + 1, so a tip regression mid-catchup is the trigger. A single fetcher is sufficient; the concurrent-spawner race in #466 can amplify it (a slower fetcher rewinding from_height past the tip) but is not required. The task is spawned via tokio::spawn in monitor_events_item with no cancellation token, so once spinning it does not stop until process restart.

Steps to reproduce

  1. Start history catchup on a watch contract (node behind the tip).
  2. While fetch_history_events is running, make the indexer tip regress below the cursor's from_height (subgraph reindex/reset or replica lag).
  3. Observe the task: the "will finish" log line repeats with no sleep between iterations; CPU usage of the task pins until process restart.

Proof of concept (source level)

Source-only review; no PoC transaction; no live system contacted. Byte-exact at 1ff9a4a:

// event_watch_task.rs:1120-1125
            if watch_contract.from_height > current_finalized {
                info!(
                    "Contract {task_name} fetch history events will finish, as current finalize height: {current_finalized} is litter than watch from height: {}",
                    watch_contract.from_height,
                );
                continue;
            }

Sibling paths that DO sleep (same loop, for contrast):

                Ok(None) => {
                    warn!("fetch_history_events:fail to get graph sync block height, will try later, empty value returned");
                    sleep(Duration::from_millis(500)).await;
                    continue;
                }

Dupe check

No existing issue or PR covers the busy-loop (checked all 41 issues and 420 PRs as of 2026-09-14). Related but distinct: #466 covers the concurrent-spawner race that can rewind from_height; this covers the unbounded spin after a rewind or tip regression.

Security impact

Local DoS / resource griefing of the event-watch history task: CPU spin until process restart, stalling catchup progress for the affected contract. Low severity. Source only review; no PoC transactions were performed; no live system was contacted.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions