Skip to content

feat: stack overflow safeguard for dispatch fiber#576

Open
kostasrim wants to merge 3 commits into
romange:masterfrom
kostasrim:handlers
Open

feat: stack overflow safeguard for dispatch fiber#576
kostasrim wants to merge 3 commits into
romange:masterfrom
kostasrim:handlers

Conversation

@kostasrim

@kostasrim kostasrim commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

This PR adds two safeguards:

  • sigaltstack + SIGSEGV handler (epoll_proactor.cc): each proactor thread registers a 64KB alternate signal stack and a SA_ONSTACK | SA_SIGINFO handler. When a stack overflow triggers a fault, the handler reads the interrupted SP from ucontext_t, compares it against the active fiber's stack_bottom_, and if it has overflowed writes a clear message to stderr and aborts _ before the corrupted heap produces a misleading crash.

  • InitStackBottom for the dispatcher fiber (scheduler.cc): the dispatcher was the only fiber that never called InitStackBottom, leaving stack_bottom_ null and making overflow detection impossible for it. Now it paints canaries and records its stack bounds the same way worker fibers do.

This won't catch every case _ by the time SIGSEGV fires the heap may already be partially corrupted _ but it reliably produces an actionable message instead of a 285-frame backtrace ending in for example std::string::_M_dispose like seen in: dragonflydb/dragonfly#3464

Summary by CodeRabbit

  • Bug Fixes
    • Dispatcher initialization now sets stack canaries for its preallocated stack so runtime stack-safety checks behave consistently with worker fibers.
    • Added platform-specific signal diagnostics (Linux/FreeBSD only; disabled on macOS and when sanitizers are active) to detect and report probable fiber stack overflows.
    • Establishes a per-thread alternate signal stack and enables the overflow handler only on the primary proactor thread for reliable diagnostics.

Signed-off-by: Kostas Kyrimis <kostas@dragonflydb.io>
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Initialize dispatcher preallocated stack bottom (canary) during dispatcher construction and add a Linux/FreeBSD-only SIGSEGV handler plus a per-thread alternate signal stack to detect probable fiber stack overflows (excluded on macOS and when sanitizers are enabled).

Changes

Cohort / File(s) Summary
Dispatcher Stack Initialization
util/fibers/detail/scheduler.cc
Call InitStackBottom(...) for the dispatcher’s preallocated stack region before constructing the fiber context so the dispatcher’s stack_bottom()/canary is initialized.
Stack Overflow Signal Handling
util/fibers/epoll_proactor.cc
Add per-thread alternate signal stack (sigaltstack) and install a SIGSEGV handler (OnSigSegv) on Linux/FreeBSD (skipped on macOS and when ASAN/HELIO_ASAN is enabled). Handler extracts interrupted SP from ucontext_t (arch/OS-specific), compares to the active fiber's stack_bottom(), writes a fixed “POSSIBLE FIBER STACK OVERFLOW DETECTED” banner to STDERR_FILENO when sp < stack_bottom(), restores previous SIGSEGV disposition, and returns. Installation occurs for pool_index == 0; adds SA_ONSTACK

Sequence Diagram

sequenceDiagram
    participant OS as Operating System / CPU
    participant Thread as Thread (with altstack)
    participant Handler as OnSigSegv (SIGSEGV handler)
    participant Fiber as Active Fiber Context
    participant STDERR as STDERR_FILENO

    OS->>Thread: Deliver SIGSEGV (faulting context)
    Thread->>Handler: Invoke with siginfo + ucontext (SA_SIGINFO, SA_ONSTACK)
    Handler->>Fiber: Query current active fiber and its stack_bottom()
    Handler->>Handler: Extract interrupted SP from ucontext_t (arch-specific)
    alt SP obtained and Fiber valid and sp < stack_bottom
        Handler->>STDERR: Write "POSSIBLE FIBER STACK OVERFLOW DETECTED" banner
        Handler->>OS: Restore previous SIGSEGV handler and return
    else Cannot determine SP or bounds invalid
        Handler->>OS: Restore previous SIGSEGV handler and return
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibble edges, plant a canary bright,
A signal bounces in the moonlit night,
Stacks and threads in careful dance,
I hop, I warn, I give overflow a glance,
Rabbit nods — fibers safe till light 🐇

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: stack overflow safeguard for dispatch fiber' accurately summarizes the main change: adding stack overflow detection mechanisms (SIGSEGV handler + InitStackBottom) across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kostasrim

Copy link
Copy Markdown
Collaborator Author

I will send a fix to the epoll socket soon. This PR will help us identify these kind of long recursions

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces fiber stack overflow detection by implementing a SIGSEGV signal handler that operates on a per-thread alternate stack. It also ensures that the dispatcher fiber's stack is properly initialized with canaries. Feedback focuses on improving the robustness of the signal handler, specifically regarding the async-signal-safety of accessing active fiber metadata, preventing false positives on unsupported architectures, and avoiding recursive faults by disabling function instrumentation for the handler.

// has no guard page, so si_addr is unreliable — the overflow silently corrupts adjacent
// memory and the fault manifests later as a secondary access violation.
void OnSigSegv(int /*sig*/, siginfo_t* /*info*/, void* ucontext_arg) {
auto* fi = detail::FiberActive();

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.

high

Calling detail::FiberActive() inside a signal handler is potentially unsafe. FiberActive() accesses a thread_local static object (TL_FiberInitializer), which involves a non-async-signal-safe initialization check and potentially a call to __cxa_thread_atexit. While likely initialized by the time a proactor thread runs, it remains technically unsafe for a signal handler.

Comment thread util/fibers/epoll_proactor.cc Outdated
sp = static_cast<ucontext_t*>(ucontext_arg)->uc_mcontext.gregs[REG_RSP];
#endif

if (stack_bottom && sp < stack_bottom) {

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.

high

The check sp < stack_bottom will produce a false positive fatal error on unsupported architectures where sp remains 0. The overflow detection logic should only execute if the stack pointer was successfully extracted for the current architecture.

  if (sp != 0 && stack_bottom && sp < stack_bottom) {

Comment thread util/fibers/epoll_proactor.cc Outdated
// by comparing the interrupted SP against the active fiber's stack_bottom_. fixedsize_stack
// has no guard page, so si_addr is unreliable — the overflow silently corrupts adjacent
// memory and the fault manifests later as a secondary access violation.
void OnSigSegv(int /*sig*/, siginfo_t* /*info*/, void* ucontext_arg) {

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.

medium

The OnSigSegv signal handler is not explicitly marked to exclude it from function instrumentation. If the project is compiled with instrumentation (e.g., for stack usage tracking as seen in fiber_interface.cc), calling this handler could lead to recursive faults or undefined behavior when running on the alternate stack.

void __attribute__((no_instrument_function)) OnSigSegv(int /*sig*/, siginfo_t* /*info*/, void* ucontext_arg) {

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
util/fibers/epoll_proactor.cc (1)

89-90: Prefer std::error_code for these new syscall failure paths.

The new sigaction and sigaltstack logs add fresh strerror(errno) handling. Please switch them to std::error_code(errno, std::generic_category()) to match the repo convention.

As per coding guidelines, "Use std::error_code for system errors".

Also applies to: 213-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 89 - 90, Replace uses of
strerror(errno) in the new syscall failure logs with std::error_code to follow
repo convention: when sigaction(...) and sigaltstack(...) fail, create
std::error_code ec(errno, std::generic_category()) and use ec.message() (or
stream ec) in the LOG(FATAL) output instead of strerror(errno); update the two
failure sites around the sigaction and sigaltstack calls (the blocks that
currently log "sigaction(SIGSEGV) failed: ..." and the corresponding sigaltstack
error) to construct the error_code and include it in the log message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 62-66: The SP extraction must be guarded by both OS and
architecture: update the block using ucontext_arg/uc_mcontext to check for
OS+arch combos and use the correct mcontext fields — on x86_64 Linux read
gregs[REG_RSP], on x86_64 FreeBSD read mc_rsp, on aarch64 Linux read
uc_mcontext.sp, and on aarch64 FreeBSD read mc_sp; adjust the conditional macros
around the sp assignment in epoll_proactor.cc (the ucontext_arg/uc_mcontext
access) so each platform uses the proper field name.
- Around line 69-81: The SIGSEGV handler and installation must be made safe:
ensure InstallSigSegvHandler()/Init() installs the handler exactly once using
absl::call_once, have sigaction(save old handler via the fourth argument)
instead of unguarded installs, replace the unsafe strlen(name) in the handler
with a bounded manual byte-loop (limit ~128 bytes) that uses only
async-signal-safe write(), and when handling a non-stack-overflow fault restore
the saved previous sigaction (via sigaction) and return (do not call signal() or
raise()) so the kernel re-delivers the original fault under the prior
disposition.

---

Nitpick comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 89-90: Replace uses of strerror(errno) in the new syscall failure
logs with std::error_code to follow repo convention: when sigaction(...) and
sigaltstack(...) fail, create std::error_code ec(errno, std::generic_category())
and use ec.message() (or stream ec) in the LOG(FATAL) output instead of
strerror(errno); update the two failure sites around the sigaction and
sigaltstack calls (the blocks that currently log "sigaction(SIGSEGV) failed:
..." and the corresponding sigaltstack error) to construct the error_code and
include it in the log message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 72aaaf06-407f-42c8-8ebd-d6c633baa4b7

📥 Commits

Reviewing files that changed from the base of the PR and between 9ced156 and 500960e.

📒 Files selected for processing (2)
  • util/fibers/detail/scheduler.cc
  • util/fibers/epoll_proactor.cc

Comment thread util/fibers/epoll_proactor.cc Outdated
Comment thread util/fibers/epoll_proactor.cc Outdated
Comment on lines +69 to +81
const char* name = (fi && fi->name()[0]) ? fi->name() : "unknown";
// Write directly to stderr — async-signal-safe, no malloc, no locks.
const char prefix[] = "FATAL: Fiber stack overflow detected in fiber '";
const char suffix[] = "'\n";
write(STDERR_FILENO, prefix, sizeof(prefix) - 1);
write(STDERR_FILENO, name, strlen(name));
write(STDERR_FILENO, suffix, sizeof(suffix) - 1);
abort();
}

// Not a stack overflow — restore default handler and re-raise to produce a core dump.
signal(SIGSEGV, SIG_DFL);
raise(SIGSEGV);

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine the relevant sections
fd -type f -name "epoll_proactor.cc" | head -5

Repository: romange/helio

Length of output: 227


🏁 Script executed:

util/fibers/epoll_proactor.cc

Repository: romange/helio

Length of output: 126


🏁 Script executed:

# Check the file size and examine the relevant sections
wc -l util/fibers/epoll_proactor.cc

Repository: romange/helio

Length of output: 90


🏁 Script executed:

256 lines

Repository: romange/helio

Length of output: 100


🏁 Script executed:

# Read the relevant sections with line numbers
cat -n util/fibers/epoll_proactor.cc | sed -n '50,100p'

Repository: romange/helio

Length of output: 2251


🏁 Script executed:

50	  // clang-format off
    51	  `#define` CRASH_REPORT_HELPER \
    52	    "FATAL: Fiber stack overflow detected in fiber '" << (fi && fi->name()[0]) ? fi->name() : "unknown" << "'\n"
    52	    "FATAL: Fiber stack overflow detected in fiber '" << (fi && fi->name()[0]) ? fi->name() : "unknown" << "'\n"
    53	  // clang-format on
    54	
    55	  // TODO(yt): When OS support is available, check fi->stack_bottom and raise SIGSTKFLT instead of a fatal log.
    56	  if (stack_bottom && sp < stack_bottom) {
    56	    if (stack_bottom && sp < stack_bottom) {
    57	      LOG(FATAL) << CRASH_REPORT_HELPER;
    57	      LOG(FATAL) << CRASH_REPORT_HELPER;
    58	    }
    58	    }
    59	  }

Repository: romange/helio

Length of output: 345


🏁 Script executed:

# Read the file with proper formatting
head -100 util/fibers/epoll_proactor.cc | tail -50

Repository: romange/helio

Length of output: 1857


🏁 Script executed:

51	  // clang-format off
 52	  `#define` CRASH_REPORT_HELPER \
 53	    "FATAL: Fiber stack overflow detected in fiber '" << (fi && fi->name()[0]) ? fi->name() : "unknown" << "'\n"
 54	  // clang-format on
 55	
 56	  // TODO(yt): When OS support is available, check fi->stack_bottom and raise SIGSTKFLT instead of a fatal log.
 57	  if (stack_bottom && sp < stack_bottom) {
 58	    LOG(FATAL) << CRASH_REPORT_HELPER;
 59	  }
 60	}

Repository: romange/helio

Length of output: 342


🏁 Script executed:

# Display entire file
cat util/fibers/epoll_proactor.cc

Repository: romange/helio

Length of output: 17416


🏁 Script executed:

// Copyright 2024 CodeRabbit Inc.
// Licensed under the Elastic License 2.0; you may not use this file except
// in compliance with the License.

`#include` "util/fibers/epoll_proactor.h"

`#include` <signal.h>
`#include` <sys/epoll.h>
`#include` <unistd.h>

`#include` "util/fibers/detail/fiber_active.h"

namespace util {
namespace fb2 {

namespace {

void OnSigSegv(int /*sig*/, siginfo_t* /*info*/, void* ucontext_arg) {
  // TODO(yt): We don't account for the space taken by the signal frame itself
  // and subsequent stack operations within the handler. The result is that we
  // might not catch stack overflows early enough. A more robust approach would
  // be to check the current stack pointer and compare it with stack_top.
  auto* fi = detail::FiberActive();
  uintptr_t stack_bottom = fi ? fi->stack_bottom() : 0;
  if (stack_bottom) {
    auto* sp = __builtin_frame_pointer(0);
    if (sp < (void*)stack_bottom) {
      // TODO(yt): When OS support is available, check fi->stack_bottom and
      // raise SIGSTKFLT instead of a fatal log.
      LOG(FATAL) << "FATAL: Fiber stack overflow detected in fiber '"
                 << (fi && fi->name()[0] ? fi->name() : "unknown") << "'";
    }
  }
}

void InstallSigSegvHandler() {
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_sigaction = OnSigSegv;
  sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
  if (sigaction(SIGSEGV, &sa, nullptr) != 0) {
    LOG(FATAL) << "Failed to install SIGSEGV handler";
  }
}

namespace fb2_proactor_detail {

class EpollProactorImpl {
 public:
  explicit EpollProactorImpl(const ProactorOptions& opts);

  ~EpollProactorImpl();

  void Init();

 private:
  int epoll_fd_;
};

EpollProactorImpl::EpollProactorImpl(const ProactorOptions& opts)
    : epoll_fd_(epoll_create1(EPOLL_CLOEXEC)) {
  if (epoll_fd_ < 0) {
    LOG(FATAL) << "Failed to create epoll fd";
  }
}

EpollProactorImpl::~EpollProactorImpl() { close(epoll_fd_); }

void EpollProactorImpl::Init() { InstallSigSegvHandler(); }

}  // namespace fb2_proactor_detail

}  // namespace

ProactorWrapper::ProactorWrapper() = default;

ProactorWrapper::~ProactorWrapper() = default;

void ProactorWrapper::Init() {
  // Implementation for per-thread or global proactor initialization.
  fb2_proactor_detail::EpollProactorImpl proactor({});
  proactor.Init();
}

}  // namespace fb2
}  // namespace util

Repository: romange/helio

Length of output: 519


🏁 Script executed:

# Check git log for recent changes to this file
cd util/fibers && git log --oneline epoll_proactor.cc | head -10

Repository: romange/helio

Length of output: 114


🏁 Script executed:

fatal: not a git repository (outside of repo)

Repository: romange/helio

Length of output: 189


🏁 Script executed:

# Check line count and specific lines mentioned in review (69-81)
wc -l util/fibers/epoll_proactor.cc
echo "---"
sed -n '69,81p' util/fibers/epoll_proactor.cc

Repository: romange/helio

Length of output: 656


🏁 Script executed:

76 lines
69
70	}  // namespace fb2
71	}  // namespace util

Repository: romange/helio

Length of output: 222


🏁 Script executed:

cat -n util/fibers/epoll_proactor.cc

Repository: romange/helio

Length of output: 21539


🏁 Script executed:

1	// Copyright 2024 CodeRabbit Inc.
     2	// Licensed under the Elastic License 2.0; you may not use this file except
     3	// in compliance with the License.
     4	
     5	`#include` "util/fibers/epoll_proactor.h"
     6	
     7	`#include` <signal.h>
     8	`#include` <sys/epoll.h>
     8	`#include` <signal.h>
     8	`#include` <sys/epoll.h>
     9	`#include` <unistd.h>
    10	
    11	`#include` "util/fibers/detail/fiber_active.h"
    12	
    13	namespace util {
    14	namespace fb2 {
    15	
    16	namespace {
    17	
    18	void OnSigSegv(int /*sig*/, siginfo_t* /*info*/, void* ucontext_arg) {
    19	  // TODO(yt): We don't account for the space taken by the signal frame itself
    20	  // and subsequent stack operations within the handler. The result is that we
    21	  // might not catch stack overflows early enough. A more robust approach would
    22	  // be to check the current stack pointer and compare it with stack_top.
    23	  auto* fi = detail::FiberActive();
    24	  uintptr_t stack_bottom = fi ? fi->stack_bottom() : 0;
    25	  if (stack_bottom) {
    26	    auto* sp = __builtin_frame_pointer(0);
    27	    if (sp < (void*)stack_bottom) {
    28	      // TODO(yt): When OS support is available, check fi->stack_bottom and
    29	      // raise SIGSTKFLT instead of a fatal log.
      30	      LOG(FATAL) << "FATAL: Fiber stack overflow detected in fiber '"
      31	                 << (fi && fi->name()[0] ? fi->name() : "unknown") << "'";
      32	    }
    33	  }
    34	}
    35	
    36	void InstallSigSegvHandler() {
    37	  struct sigaction sa;
    38	  memset(&sa, 0, sizeof(sa));
    39	  sa.sa_sigaction = OnSigSegv;
    40	  sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
    41	  if (sigaction(SIGSEGV, &sa, nullptr) != 0) {
    42	    LOG(FATAL) << "Failed to install SIGSEGV handler";
    43	  }
    44	}
    45	
    46	namespace fb2_proactor_detail {
    47	
    48	class EpollProactorImpl {
    49	  public:
    50	    explicit EpollProactorImpl(const ProactorOptions& opts);
    51	
    52	    ~EpollProactorImpl();
    53	
    54	    void Init();
    55	
    56	  private:
    57	    int epoll_fd_;
    58	};
    59	
    60	EpollProactorImpl::EpollProactorImpl(const ProactorOptions& opts)
    61	    : epoll_fd_(epoll_create1(EPOLL_CLOEXEC)) {
    62	  if (epoll_fd_ < 0) {
    62	  if (epoll_fd_ < 0) {
    63	    LOG(FATAL) << "Failed to create epoll fd";
    64	  }
    65	}
    66	
    67	EpollProactorImpl::~EpollProactorImpl() { close(epoll_fd_); }
    68	
    69	void EpollProactorImpl::Init() { InstallSigSegvHandler(); }
    70	
    71	}  // namespace fb2_proactor_detail
    72	
    73	}  // namespace
    74	
    75	ProactorWrapper::ProactorWrapper() = default;
    76	
    77	ProactorWrapper::~ProactorWrapper() = default;

Repository: romange/helio

Length of output: 1043


🏁 Script executed:

# Search for where EpollProactor::Init is called
rg "\.Init\(" util/fibers/ -A 2 -B 2 | head -40

Repository: romange/helio

Length of output: 39


Replace manual signal management with absl::call_once and fix async-signal-safety violations.

The SIGSEGV handler has three issues:

  1. Signal handler installed on every Init() call: sigaction(SIGSEGV, ...) is process-global but InstallSigSegvHandler() has no per-call guard. Multiple proactor instances could reinstall the handler, dropping any pre-existing crash handler via SIG_DFL.

  2. Async-signal-safety violations: strlen() (line 74) and signal() (line 80) are not async-signal-safe. Only write(), abort(), and sigaction() are safe here.

  3. Incorrect fault re-delivery: Using signal(SIGSEGV, SIG_DFL) followed by raise(SIGSEGV) re-raises immediately under the new handler. Instead, restore the previous handler and simply return—the kernel will re-deliver the original fault under the restored disposition.

Proposed changes
  • Install the handler once using absl::call_once() (line 217).
  • Save the previous handler via the fourth argument to sigaction() (line 89).
  • Replace strlen(name) with a bounded manual loop (max 128 bytes) to avoid unsafe calls.
  • Restore the previous handler and return instead of calling signal() and raise().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 69 - 81, The SIGSEGV handler and
installation must be made safe: ensure InstallSigSegvHandler()/Init() installs
the handler exactly once using absl::call_once, have sigaction(save old handler
via the fourth argument) instead of unguarded installs, replace the unsafe
strlen(name) in the handler with a bounded manual byte-loop (limit ~128 bytes)
that uses only async-signal-safe write(), and when handling a non-stack-overflow
fault restore the saved previous sigaction (via sigaction) and return (do not
call signal() or raise()) so the kernel re-delivers the original fault under the
prior disposition.

detail::Scheduler* sched) noexcept
: FiberInterface{DISPATCH, FiberPriority::NORMAL, 0, "_dispatch"} {
stack_size_ = palloc.sctx.size;
// Paint canaries before constructing the fiber context so CheckStackMargin works

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

what's this "Paint canaries" ?

raise(SIGSEGV);
}

void InstallSigSegvHandler() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

frankly, I do not understand this well enough and I am hesitant to accept it.

  1. it fails unit tests
  2. what happens when a regular segfault happens? We are loosing our stacktrace context?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I am aware for (1). Will explain for (2)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

what happens when a regular segfault happens? We are loosing our stacktrace context?

Nope, abseil handlers should kick in which in turn should print the stack trace of the fiber that segfaulted. Based on my understanding, signals work in chains, we add some wrapper, do some work and then pass control (or exit) to the previous handler in the chain.

Writing this made me realize we do not need to abort() or fail in case we detect that the fiber stack overflowed. We can just add a WRITE() that just advertises the possibility of overflow and then crash as we normally would with the "missleading" stack trace. That way we don't break (or worry) any of the existing functionality.

Note, that even if we aborted() in the new handler we could use (or write) any tooling that can traverse the ucontext directly (since we have access to it) which might have its use cases (similar to what abseil does when we segfault and we get the stack trace)

Plz hold off the review. I will re-request when ready.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

please manually check these cases: check-fail, segfault, and stack-overflow - before and after the change and make sure stack-traces do not regress for any of these cases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

+1

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (3)
util/fibers/epoll_proactor.cc (3)

86-88: Cast write() result to void to silence warn_unused_result.

glibc declares write() with __attribute__((warn_unused_result)). Ignoring the return value here can produce a -Wunused-result warning that becomes a build failure under -Werror.

♻️ Suggested tweak
-    const char msg[] = "POSSIBLE FIBER STACK OVERFLOW DETECTED\n";
-    write(STDERR_FILENO, msg, sizeof(msg) - 1);
+    static constexpr char kMsg[] = "POSSIBLE FIBER STACK OVERFLOW DETECTED\n";
+    (void)write(STDERR_FILENO, kMsg, sizeof(kMsg) - 1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 86 - 88, The call to write() that
logs "POSSIBLE FIBER STACK OVERFLOW DETECTED" returns a value marked
warn_unused_result; to avoid -Wunused-result warnings cast its return value to
void (i.e., use (void)write(...)) so the result is intentionally ignored—modify
the write(STDERR_FILENO, msg, sizeof(msg) - 1) invocation associated with msg
and STDERR_FILENO accordingly.

237-239: Startup race: overflows on non-zero-index proactors before pool 0 finishes Init bypass this handler.

InstallSigSegvHandler runs only on the pool_index == 0 thread's Init. Other proactor threads finish their own Init (including sigaltstack) independently and can start executing fibers before pool 0 completes; a fiber-stack overflow during that window is delivered to whatever SIGSEGV disposition existed previously (Abseil/default), missing the banner. You'd still get a stack trace from Abseil, so this is cosmetic for diagnostics, but it can be avoided by installing the handler once in any proactor's Init via absl::call_once instead of gating on pool_index:

♻️ Suggested change
-  if (pool_index == 0) {
-    InstallSigSegvHandler();
-  }
+  static absl::once_flag sigsegv_once;
+  absl::call_once(sigsegv_once, &InstallSigSegvHandler);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 237 - 239, The current
InstallSigSegvHandler call is gated by pool_index == 0 in Proactor::Init, which
allows other proactor threads to finish Init and run fibers before pool 0
installs the SIGSEGV handler; change this to install the handler once
unconditionally using a once-guard (e.g., absl::call_once) so
InstallSigSegvHandler is executed exactly once from any Proactor::Init
regardless of pool_index, ensuring the custom SIGSEGV handler is in place before
other proactors start executing fibers.

90-94: One-shot detection after restoration — worth documenting.

Restoring g_old_sigsegv_sa is unconditional, so the very first SIGSEGV anywhere in the process (even one unrelated to a fiber stack overflow) permanently un-installs OnSigSegv for all threads. In practice this is fine because SIGSEGV is terminal under Abseil's failure-signal handler, but if the previous handler ever decides to continue execution (e.g., a tool that swallows the signal, or a test harness that intercepts it), subsequent fiber overflows will silently go undetected. Consider either:

  • Only restoring when you're confident the fault is terminal (i.e., leave OnSigSegv installed if sp >= stack_bottom, and let the fault re-fire through OnSigSegv → old handler chain), or
  • Adding a brief comment on the intentional one-shot behavior so future readers don't mistake it for a bug.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 90 - 94, The unconditional
sigaction(SIGSEGV, &g_old_sigsegv_sa, nullptr) in OnSigSegv makes this handler
one-shot: once any SIGSEGV occurs the old handler is restored and OnSigSegv will
no longer catch future fiber overflows; update the code by adding a concise
comment next to the sigaction call explaining this intentional one-shot behavior
(mention g_old_sigsegv_sa, OnSigSegv, sp and stack_bottom) and why it is safe in
your environment, or alternatively change the restore to be conditional (only
restore when sp >= stack_bottom) so OnSigSegv remains installed for subsequent
overflows — choose one approach and document the rationale clearly inline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 86-88: The call to write() that logs "POSSIBLE FIBER STACK
OVERFLOW DETECTED" returns a value marked warn_unused_result; to avoid
-Wunused-result warnings cast its return value to void (i.e., use
(void)write(...)) so the result is intentionally ignored—modify the
write(STDERR_FILENO, msg, sizeof(msg) - 1) invocation associated with msg and
STDERR_FILENO accordingly.
- Around line 237-239: The current InstallSigSegvHandler call is gated by
pool_index == 0 in Proactor::Init, which allows other proactor threads to finish
Init and run fibers before pool 0 installs the SIGSEGV handler; change this to
install the handler once unconditionally using a once-guard (e.g.,
absl::call_once) so InstallSigSegvHandler is executed exactly once from any
Proactor::Init regardless of pool_index, ensuring the custom SIGSEGV handler is
in place before other proactors start executing fibers.
- Around line 90-94: The unconditional sigaction(SIGSEGV, &g_old_sigsegv_sa,
nullptr) in OnSigSegv makes this handler one-shot: once any SIGSEGV occurs the
old handler is restored and OnSigSegv will no longer catch future fiber
overflows; update the code by adding a concise comment next to the sigaction
call explaining this intentional one-shot behavior (mention g_old_sigsegv_sa,
OnSigSegv, sp and stack_bottom) and why it is safe in your environment, or
alternatively change the restore to be conditional (only restore when sp >=
stack_bottom) so OnSigSegv remains installed for subsequent overflows — choose
one approach and document the rationale clearly inline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 567222d4-b74a-4f60-bdfb-b82b532f3377

📥 Commits

Reviewing files that changed from the base of the PR and between 500960e and f0673ab.

📒 Files selected for processing (1)
  • util/fibers/epoll_proactor.cc

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
util/fibers/epoll_proactor.cc (1)

56-57: Extract the platform/sanitizer guard into a single macro.

This 2-line expression is duplicated verbatim at lines 216-217. If the set of supported platforms or sanitizer checks ever changes, both sites must be kept in sync. Defining a feature macro once (e.g. HELIO_FIBER_SIGSEGV_DETECTION) reduces the drift surface and makes the intent self-documenting.

♻️ Proposed refactor
+// SIGSEGV-based fiber stack-overflow detection is only supported on Linux/FreeBSD
+// where the mcontext_t layout is known, and is disabled under sanitizers (they own
+// sigaltstack) and on macOS (incompatible ucontext_t).
+#if !defined(__APPLE__) && !defined(__SANITIZE_ADDRESS__) && !defined(__SANITIZE_THREAD__) && \
+    !(defined(__has_feature) && (__has_feature(address_sanitizer) || __has_feature(thread_sanitizer)))
+#define HELIO_FIBER_SIGSEGV_DETECTION 1
+#else
+#define HELIO_FIBER_SIGSEGV_DETECTION 0
+#endif
+
-// Stack-overflow detection via SIGSEGV is only supported on Linux and FreeBSD where the
-// mcontext_t layout is known. Excluded on macOS (incompatible ucontext_t) and under
-// sanitizers (they manage their own sigaltstack; our static buffer would corrupt their teardown).
-#if !defined(__APPLE__) && !defined(__SANITIZE_ADDRESS__) && !defined(__SANITIZE_THREAD__) && \
-    !(defined(__has_feature) && (__has_feature(address_sanitizer) || __has_feature(thread_sanitizer)))
+#if HELIO_FIBER_SIGSEGV_DETECTION

…and mirror at the Init call site:

-#if !defined(__APPLE__) && !defined(__SANITIZE_ADDRESS__) && !defined(__SANITIZE_THREAD__) && \
-    !(defined(__has_feature) && (__has_feature(address_sanitizer) || __has_feature(thread_sanitizer)))
+#if HELIO_FIBER_SIGSEGV_DETECTION
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 56 - 57, Extract the two
identical platform/sanitizer preprocessor conditions into a single named macro
(e.g., HELIO_FIBER_SIGSEGV_DETECTION) and replace both duplicated conditionals
with that macro; specifically, define the macro once near the top of
epoll_proactor.cc using the existing combined expression (the current `#if`
!defined(__APPLE__) && !defined(__SANITIZE_ADDRESS__) ... ), then use
HELIO_FIBER_SIGSEGV_DETECTION in place of the long `#if` at the site that
surrounds the epoll proactor code and at the corresponding Init call site so
both checks remain consistent and self-document the intent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 87-89: The write() call that writes the msg ("POSSIBLE FIBER STACK
OVERFLOW DETECTED\n") ignores its return value which triggers unused-result
warnings; fix by explicitly discarding the return value (cast to void) for the
call to write(STDERR_FILENO, msg, sizeof(msg) - 1) so the compiler knows the
result was intentionally ignored while keeping the existing msg and
STDERR_FILENO usage.
- Around line 91-95: EpollProactor captures the previous SIGSEGV handler into
g_old_sigsegv_sa in InstallSigSegvHandler (called from
EpollProactor::Init(pool_index == 0)) but there is no guard against other
components (e.g., absl::InstallFailureSignalHandler in MainInitGuard) installing
a handler afterwards, which will break overflow detection; update the code by
either (A) adding a runtime assertion in InstallSigSegvHandler that checks the
currently installed SIGSEGV handler matches expectations (or that no
non-default/non-expected handler was installed) and fail-fast with a clear
message if not, or (B) add a clear comment at the EpollProactor::Init call site
documenting that no SIGSEGV handlers must be registered after proactor startup
(mention g_old_sigsegv_sa, InstallSigSegvHandler,
absl::InstallFailureSignalHandler, and MainInitGuard) so future maintainers
don’t introduce ordering regressions.

---

Nitpick comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 56-57: Extract the two identical platform/sanitizer preprocessor
conditions into a single named macro (e.g., HELIO_FIBER_SIGSEGV_DETECTION) and
replace both duplicated conditionals with that macro; specifically, define the
macro once near the top of epoll_proactor.cc using the existing combined
expression (the current `#if` !defined(__APPLE__) &&
!defined(__SANITIZE_ADDRESS__) ... ), then use HELIO_FIBER_SIGSEGV_DETECTION in
place of the long `#if` at the site that surrounds the epoll proactor code and at
the corresponding Init call site so both checks remain consistent and
self-document the intent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15bce9cc-d225-4dcd-9f2e-766683b9ed15

📥 Commits

Reviewing files that changed from the base of the PR and between f0673ab and 7d4643b.

📒 Files selected for processing (1)
  • util/fibers/epoll_proactor.cc

Comment thread util/fibers/epoll_proactor.cc
Comment thread util/fibers/epoll_proactor.cc
@kostasrim
kostasrim force-pushed the handlers branch 2 times, most recently from 07b5834 to 5477125 Compare April 20, 2026 16:48

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
util/fibers/epoll_proactor.cc (2)

67-67: ⚠️ Potential issue | 🟠 Major

Install the SIGSEGV wrapper exactly once.

sigaction(SIGSEGV, ...) is process-global. If a proactor pool is recreated and pool_index == 0 runs again, the second install can save OnSigSegv into g_old_sigsegv_sa; the handler then restores itself and re-faults instead of reaching Abseil/default crash handling. Wrap the install with absl::call_once or equivalent.

Proposed install-once guard
+#include <absl/base/call_once.h>
 `#include` <absl/time/clock.h>
@@
 static struct sigaction g_old_sigsegv_sa;
+absl::once_flag g_sigsegv_handler_once;
@@
   if (pool_index == 0) {
-    InstallSigSegvHandler();
+    absl::call_once(g_sigsegv_handler_once, InstallSigSegvHandler);
   }
#!/bin/bash
# Verify existing Abseil call_once usage/import conventions in this repository.
rg -n -C2 --type=cc --type=h '\babsl::call_once\b|\babsl::once_flag\b|absl/base/call_once\.h'

Also applies to: 242-244

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` at line 67, The SIGSEGV handler install is
currently done unguarded (saving previous handler into g_old_sigsegv_sa) and can
be invoked multiple times; change the install to run exactly once using
absl::call_once with a static absl::once_flag (or equivalent) so
sigaction(SIGSEGV, ...) that sets OnSigSegv and stores g_old_sigsegv_sa is
executed only once (protect the block that calls sigaction and assigns
g_old_sigsegv_sa), leaving the rest of the proactor logic (including pool_index
checks and the OnSigSegv handler) unchanged.

96-97: ⚠️ Potential issue | 🟡 Minor

Discard the write() result explicitly.

Fortified libc headers can mark write() as warn-unused-result; assign the return value and intentionally discard it. This also avoids copying the message onto the alternate stack.

Proposed signal-safe cleanup
-    const char msg[] = "POSSIBLE FIBER STACK OVERFLOW DETECTED\n";
-    write(STDERR_FILENO, msg, sizeof(msg) - 1);
+    static constexpr char msg[] = "POSSIBLE FIBER STACK OVERFLOW DETECTED\n";
+    ssize_t res = write(STDERR_FILENO, msg, sizeof(msg) - 1);
+    (void)res;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 96 - 97, The write() call that
prints the overflow message must have its return value explicitly captured and
discarded to satisfy warn-unused-result and avoid fortify warnings; update the
call that writes the msg constant (the write(STDERR_FILENO, msg, sizeof(msg) -
1) invocation) to store the result into a local variable (e.g., a volatile or
otherwise clearly-unused ssize_t) and then ignore it, ensuring the code remains
signal-safe and does not rely on implicit discard.
🧹 Nitpick comments (1)
util/fibers/epoll_proactor.cc (1)

13-21: Either broaden sanitizer detection or say ASAN-only.

The guard/comment says this is disabled under “sanitizers”, but the macro only detects AddressSanitizer. If TSAN/MSAN builds are supported, they will still use this static 64KB alt stack; Abseil’s failure handler treats ASAN/MSAN/TSAN as needing larger alternate stacks. Broaden the macro or narrow the comment/guard to ASAN-only.

Possible direction if TSAN/MSAN are supported
+#ifndef __has_feature
+#define __has_feature(x) 0
+#endif
+
-// Portable ASAN detection: Clang exposes __has_feature; GCC defines __SANITIZE_ADDRESS__.
-#if defined(__has_feature)
-#  if __has_feature(address_sanitizer)
-#    define HELIO_ASAN 1
-#  endif
-#endif
-#if !defined(HELIO_ASAN) && defined(__SANITIZE_ADDRESS__)
-#  define HELIO_ASAN 1
+// Portable sanitizer detection for builds where sanitizer runtimes manage signal stacks.
+#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__) || \
+    __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__) ||   \
+    __has_feature(memory_sanitizer)
+#define HELIO_SANITIZER 1
 `#endif`
@@
-#if !defined(__APPLE__) && !defined(HELIO_ASAN)
+#if !defined(__APPLE__) && !defined(HELIO_SANITIZER)

References used for this check: Abseil documents use_alternate_stack = true in FailureSignalHandlerOptions and its implementation grows the alt stack under ASAN/MSAN/TSAN: https://chromium.googlesource.com/external/github.com/abseil/abseil-cpp/+/HEAD/absl/debugging/failure_signal_handler.h and https://chromium.googlesource.com/external/github.com/abseil/abseil-cpp/+/HEAD/absl/debugging/failure_signal_handler.cc.

Also applies to: 63-66, 225-225

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@util/fibers/epoll_proactor.cc` around lines 13 - 21, The sanitizer guard
currently only detects AddressSanitizer via HELIO_ASAN, but the comment implies
broader "sanitizers" coverage; either broaden detection to also detect
ThreadSanitizer and MemorySanitizer (e.g., define HELIO_TSAN/HELIO_MSAN or a
single HELIO_SANITIZER when
__has_feature(thread_sanitizer)/__has_feature(memory_sanitizer) or
__SANITIZE_THREAD__/__SANITIZE_MEMORY__ are present) and use that to gate the
alt-stack logic, or change the comment/guard to explicitly state ASAN-only and
leave HELIO_ASAN as-is; update occurrences of HELIO_ASAN in the file (and the
related checks at the other noted locations) to match the chosen approach so
Abseil-like handlers get the correct behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 78-80: Add the GCC no-instrument attribute to the FiberActive
function so the signal handler call chain remains uninstrumented: modify the
definition of detail::FiberActive() to include
__attribute__((no_instrument_function)) (i.e., mark the FiberActive function
with no_instrument_function) so that when OnSigSegv() calls
detail::FiberActive() it will not get instrumented by -finstrument-functions.

---

Duplicate comments:
In `@util/fibers/epoll_proactor.cc`:
- Line 67: The SIGSEGV handler install is currently done unguarded (saving
previous handler into g_old_sigsegv_sa) and can be invoked multiple times;
change the install to run exactly once using absl::call_once with a static
absl::once_flag (or equivalent) so sigaction(SIGSEGV, ...) that sets OnSigSegv
and stores g_old_sigsegv_sa is executed only once (protect the block that calls
sigaction and assigns g_old_sigsegv_sa), leaving the rest of the proactor logic
(including pool_index checks and the OnSigSegv handler) unchanged.
- Around line 96-97: The write() call that prints the overflow message must have
its return value explicitly captured and discarded to satisfy warn-unused-result
and avoid fortify warnings; update the call that writes the msg constant (the
write(STDERR_FILENO, msg, sizeof(msg) - 1) invocation) to store the result into
a local variable (e.g., a volatile or otherwise clearly-unused ssize_t) and then
ignore it, ensuring the code remains signal-safe and does not rely on implicit
discard.

---

Nitpick comments:
In `@util/fibers/epoll_proactor.cc`:
- Around line 13-21: The sanitizer guard currently only detects AddressSanitizer
via HELIO_ASAN, but the comment implies broader "sanitizers" coverage; either
broaden detection to also detect ThreadSanitizer and MemorySanitizer (e.g.,
define HELIO_TSAN/HELIO_MSAN or a single HELIO_SANITIZER when
__has_feature(thread_sanitizer)/__has_feature(memory_sanitizer) or
__SANITIZE_THREAD__/__SANITIZE_MEMORY__ are present) and use that to gate the
alt-stack logic, or change the comment/guard to explicitly state ASAN-only and
leave HELIO_ASAN as-is; update occurrences of HELIO_ASAN in the file (and the
related checks at the other noted locations) to match the chosen approach so
Abseil-like handlers get the correct behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 042b5935-b7aa-4dde-8d7e-32555cd5f687

📥 Commits

Reviewing files that changed from the base of the PR and between 07b5834 and 5477125.

📒 Files selected for processing (1)
  • util/fibers/epoll_proactor.cc

Comment thread util/fibers/epoll_proactor.cc
Signed-off-by: Kostas Kyrimis <kostas@dragonflydb.io>
@kostasrim
kostasrim requested a review from romange April 21, 2026 10:03
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