Skip to content

Conversation

@strike-kokhotnikov
Copy link

Description

Sometimes it happens, that timer handlers are destroyed, but it still can run in timer thread. Current solution with lock_timer can't prevent this in case of closing the library. The new solution uses mutex for every handler and prevent execution of the handler in case of destruction, and pause destruction, when the handler is running.

What

Fix crash with dangling pointers in timers.

Change type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Tests
  • Other

Check list

  • Code follows the style de facto guidelines of this project
  • Comments have been inserted in hard to understand places
  • Documentation has been updated (if necessary)
  • Test has been added (if possible)

@svc-nbu-swx-media
Copy link

Can one of the admins verify this patch?

@greptile-apps
Copy link

greptile-apps bot commented Nov 14, 2025

Greptile Overview

Greptile Summary

Replaces per-node recursive lock with per-handler mutex and atomic flag to prevent timer handlers from executing on destroyed objects during library shutdown. The solution moves synchronization from timer_node_t into the timer_handler base class, using std::atomic<timer_handler*> for handler pointers and a new safe_handle_timer_expired() method that checks destruction state before execution.

Key changes:

  • Added lock_spin m_handle_mutex and std::atomic<bool> m_destroy_in_progress to timer_handler base class
  • Changed timer_node_t::handler from raw pointer to std::atomic<timer_handler*>
  • Removed lock_spin_recursive lock_timer from timer_node_t structure
  • Destructor now waits for any in-progress handler execution by acquiring/releasing the mutex
  • All timer expiration call sites now use safe_handle_timer_expired() instead of direct handle_timer_expired()
  • Fixed stats_publisher.cpp to register timer with this instead of global pointer and properly call set_destroying_state() before deletion

Confidence Score: 4/5

  • This PR is safe to merge with minor concerns about silent handler skipping and potential ABA problem
  • The synchronization design is fundamentally sound - the combination of atomic handler pointers, destruction flag, and mutex provides proper protection against use-after-free. However, there's a subtle concern: safe_handle_timer_expired() uses trylock() which silently skips execution if the lock is held, potentially causing timers to be silently dropped under contention. Additionally, there's a theoretical ABA problem if a handler pointer is reused at the same address after deletion, though this is unlikely in practice. The changes are well-structured and consistently applied across all timer processing code paths.
  • Most critical file is src/vma/event/timer_handler.h where the core synchronization mechanism is implemented - verify the trylock behavior is acceptable for your use case

Important Files Changed

File Analysis

Filename Score Overview
src/vma/event/timer_handler.h 4/5 Adds mutex and atomic flag to base class for safe handler synchronization during destruction
src/vma/event/delta_timer.h 5/5 Changes handler pointer to atomic, removes recursive lock from timer_node_t structure
src/vma/event/delta_timer.cpp 5/5 Updates timer processing to use atomic loads and safe_handle_timer_expired for thread-safe execution
src/vma/event/event_handler_manager.cpp 5/5 Removes lock_timer initialization, adds set_destroying_state check in unregister_timers_event_and_delete
src/vma/sock/sockinfo_tcp.cpp 5/5 Adds null check and uses safe_handle_timer_expired in tcp_timers_collection
src/stats/stats_publisher.cpp 5/5 Fixes timer registration to use this instead of global, adds proper destruction with set_destroying_state

Sequence Diagram

sequenceDiagram
    participant App as Application Thread
    participant Timer as Timer Thread
    participant Handler as timer_handler Object
    participant Node as timer_node_t
    
    Note over App,Node: Registration Phase
    App->>Timer: register_timer_event(handler)
    Timer->>Node: allocate timer_node_t
    Timer->>Node: handler.store(handler_ptr)
    
    Note over App,Node: Normal Timer Expiration
    Timer->>Timer: process_registered_timers()
    Timer->>Node: handler_ptr = handler.load()
    Timer->>Handler: safe_handle_timer_expired(data)
    Handler->>Handler: trylock() m_handle_mutex
    Handler->>Handler: check m_destroy_in_progress
    alt Lock acquired and not destroying
        Handler->>Handler: handle_timer_expired(data)
        Handler->>Handler: unlock()
    else Lock failed or destroying
        Handler->>Timer: skip execution
    end
    
    Note over App,Node: Destruction Phase
    App->>Handler: set_destroying_state(true)
    Handler->>Handler: m_destroy_in_progress.exchange(true)
    App->>Timer: unregister_timers_event_and_delete()
    Timer->>Timer: remove_all_timers(handler)
    Timer->>Node: handler.store(nullptr)
    Timer->>Handler: delete handler
    Handler->>Handler: ~timer_handler()
    Handler->>Handler: lock() m_handle_mutex (waits if timer running)
    Handler->>Handler: unlock()
    Note over Handler: Safe to destroy - no timers running
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (7)

  1. src/vma/event/delta_timer.cpp, line 97 (link)

    syntax: comparing atomic pointer without .load() - should be node->handler.load() == handler

  2. src/vma/event/delta_timer.cpp, line 105 (link)

    syntax: comparing atomic pointer without .load() - should be node->handler.load() != handler

  3. src/vma/event/delta_timer.cpp, line 112 (link)

    syntax: assigning to atomic pointer without .store() - should be node->handler.store(NULL) or node->handler.store(nullptr)

  4. src/vma/event/delta_timer.cpp, line 128 (link)

    syntax: comparing atomic pointer without .load() - should be node->handler.load() == handler

  5. src/vma/event/delta_timer.cpp, line 133 (link)

    syntax: comparing atomic pointer without .load() - should be node_tmp->handler.load() != handler

  6. src/vma/event/delta_timer.cpp, line 139 (link)

    syntax: assigning to atomic pointer without .store() - should be node_tmp->handler.store(nullptr)

  7. src/vma/event/delta_timer.cpp, line 57 (link)

    syntax: assigning to atomic pointer without .store() - should be node->handler.store(handler)

10 files reviewed, 8 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 43 to 50
void safe_handle_timer_expired(void* user_data) {
if(!m_destroy_in_progress.load()) {
if (m_handle_mutex.trylock() == 0) {
handle_timer_expired(user_data);
m_handle_mutex.unlock();
}
}
}
Copy link

Choose a reason for hiding this comment

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

logic: race condition: checking m_destroy_in_progress and trylock() are not atomic - thread can pass the check but destructor can start before trylock() succeeds, allowing handle_timer_expired() to run on partially destroyed object. the check should happen after acquiring the lock:

Suggested change
void safe_handle_timer_expired(void* user_data) {
if(!m_destroy_in_progress.load()) {
if (m_handle_mutex.trylock() == 0) {
handle_timer_expired(user_data);
m_handle_mutex.unlock();
}
}
}
void safe_handle_timer_expired(void* user_data) {
if (m_handle_mutex.trylock() == 0) {
if(!m_destroy_in_progress.load()) {
handle_timer_expired(user_data);
}
m_handle_mutex.unlock();
}
}

@strike-kokhotnikov strike-kokhotnikov force-pushed the fix/kokhotnikov/stats-crash branch from fabd923 to cc0191b Compare November 14, 2025 14:52
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

m_handle_mutex.unlock();
}
}
}
Copy link

Choose a reason for hiding this comment

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

syntax: extra closing brace causes syntax error - remove this line

Suggested change
}

@strike-kokhotnikov strike-kokhotnikov force-pushed the fix/kokhotnikov/stats-crash branch from cc0191b to 2a18d4f Compare November 14, 2025 14:59
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/vma/sock/sockinfo_tcp.cpp, line 4814 (link)

    syntax: atomic assignment requires .store() - direct assignment to std::atomic may not be thread-safe

10 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

iter->handler->handle_timer_expired(iter->user_data);
timer_handler * handler = iter->handler.load();
__log_funcall("timer expired on %p", handler);
handler->safe_handle_timer_expired(iter->user_data);
Copy link

Choose a reason for hiding this comment

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

logic: missing null check - handler can be null if timer was removed concurrently

Suggested change
handler->safe_handle_timer_expired(iter->user_data);
if (handler) {
handler->safe_handle_timer_expired(iter->user_data);
}

@strike-kokhotnikov strike-kokhotnikov force-pushed the fix/kokhotnikov/stats-crash branch from 2a18d4f to d5b2b9e Compare November 14, 2025 15:09
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

}
m_handle_mutex.unlock();
}
}
Copy link

Choose a reason for hiding this comment

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

style: inconsistent indentation - uses spaces instead of tabs like the rest of the file

Suggested change
}
}

@strike-kokhotnikov strike-kokhotnikov force-pushed the fix/kokhotnikov/stats-crash branch from d5b2b9e to 3f811e8 Compare November 14, 2025 15:15
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Additional Comments (1)

  1. src/vma/event/delta_timer.cpp, line 31-32 (link)

    syntax: !_node_->handler doesn't compile - handler is now std::atomic<timer_handler*> and requires .load()

10 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@strike-kokhotnikov strike-kokhotnikov force-pushed the fix/kokhotnikov/stats-crash branch from 9033bcf to 16f6842 Compare November 14, 2025 16:04
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

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.

2 participants