Skip to content

fatal handler: don't let a concurrent fatal mask the first one - #22671

Merged
stelfrag merged 3 commits into
netdata:masterfrom
stelfrag:fatal-handler-concurrent-guard
Jun 11, 2026
Merged

fatal handler: don't let a concurrent fatal mask the first one#22671
stelfrag merged 3 commits into
netdata:masterfrom
stelfrag:fatal-handler-concurrent-guard

Conversation

@stelfrag

@stelfrag stelfrag commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator
Summary

netdata_logger_fatal()'s recursion guard used a single process-wide counter, so it couldn't tell apart two different situations:

  • a thread re-entering fatal() while handling its own fatal (a real fatal-path bug), and
  • a different thread calling fatal() concurrently.

Make the guard thread-aware:

  • per-thread flag → same-thread re-entrancy still recursive_fatal_abort() (unchanged; it's a genuine bug);
  • process-wide counter → a concurrent second fatal waits briefly for the first to finish writing, then exits quietly via _exit(1) instead of aborting, so it no longer surfaces as a separate crash.

Summary by cubic

Make the fatal handler thread-aware so a concurrent fatal can't mask the first crash. Same-thread re-entry still aborts; other threads log once, wait briefly, then exit to avoid a second crash and stderr deadlocks.

  • Bug Fixes
    • Add a per-thread re-entrancy flag and a process-wide counter in netdata_logger_fatal() to distinguish recursion vs concurrency.
    • On concurrent fatal, bypass stdio and write to stderr using write() with corrected size calculation; wait briefly for the first to finish, then exit via _exit(1) (not abort).

Written for commit b6d3d19. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 1 file

Confidence score: 3/5

  • In src/libnetdata/log/nd_log.c, the concurrent fatal path can still hang inside fprintf/fflush(stderr), so a process that should hit the bounded wait and _exit(1) may instead block indefinitely during a crash, delaying recovery or restart. Before merging, make the fatal-path emission non-blocking (or bypass stdio entirely) so _exit(1) remains guaranteed under contention.
Architecture diagram
sequenceDiagram
    participant T1 as Thread A
    participant T2 as Thread B
    participant Fatal as netdata_logger_fatal()
    participant Stderr as stderr
    participant Exit as Process Exit

    Note over T1,Exit: Fatal handler - thread-safe concurrency control

    T1->>Fatal: call fatal()
    Fatal->>Fatal: check this_thread_in_fatal flag
    alt Same-thread re-entry (this_thread_in_fatal true)
        Fatal->>Stderr: log "RECURSIVE FATAL" message
        Fatal->>Fatal: call recursive_fatal_abort()
        Fatal-->>Exit: abort process (crash)
    else First entry for this thread
        Fatal->>Fatal: set this_thread_in_fatal = true
        Fatal->>Fatal: atomic increment threads_in_fatal
        alt Threads_in_fatal > 1 (concurrent fatal from other thread)
            Fatal->>Stderr: log "CONCURRENT FATAL" message
            Fatal->>Fatal: sleep(2) - wait for first fatal to finish
            Fatal->>Exit: _exit(1) - quiet exit, no abort
        else First thread in fatal (threads_in_fatal == 1)
            Fatal->>Fatal: proceed with logging fatal event
            Note over Fatal: Write fatal info to daemon_status_file
            Fatal->>Fatal: call fatal_abort_internal_checks()
            Fatal-->>Exit: abort process with first crash
        end
    end

    Note over T1,T2: Concurrent scenario - second thread arrives

    T2->>Fatal: call fatal() while T1 already in fatal
    Fatal->>Fatal: check this_thread_in_fatal flag (false for T2)
    Fatal->>Fatal: set this_thread_in_fatal = true (T2)
    Fatal->>Fatal: atomic increment threads_in_fatal (now 2)
    alt threads_in_fatal > 1
        Fatal->>Stderr: log "CONCURRENT FATAL" for T2
        Fatal->>Fatal: sleep(2) - wait for T1 to finish
        Fatal->>Exit: _exit(1) - T2 exits quietly
    end

    Note over T1: Meanwhile T1 continues
    T1->>Fatal: complete fatal handling
    T1->>Exit: abort process with first crash details
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/libnetdata/log/nd_log.c Outdated
…deadlocks on `stderr` lock during multi-threaded fatal scenarios.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/libnetdata/log/nd_log.c Outdated
@sonarqubecloud

Copy link
Copy Markdown

@stelfrag
stelfrag marked this pull request as ready for review June 11, 2026 10:23

@thiagoftsm thiagoftsm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues happening after hours running. LGTM!

@stelfrag
stelfrag merged commit cce16fe into netdata:master Jun 11, 2026
156 checks passed
@stelfrag
stelfrag deleted the fatal-handler-concurrent-guard branch June 11, 2026 14:21
@stelfrag stelfrag mentioned this pull request Jun 22, 2026
Ferroin pushed a commit that referenced this pull request Jul 15, 2026
* Introduce thread-local and global re-entrancy safeguards in `netdata_logger_fatal()`

* Replace `stdio` usage with `write()` in fatal logging paths to avoid deadlocks on `stderr` lock during multi-threaded fatal scenarios.

* Fix off-by-one error in `write()` size calculation for fatal log messages

(cherry picked from commit cce16fe)
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