Skip to content

in_forward: fix 100% CPU hang and connection teardown use-after-free#12044

Merged
edsiper merged 3 commits into
masterfrom
fix-12025-priority-queue-stale-event
Jul 4, 2026
Merged

in_forward: fix 100% CPU hang and connection teardown use-after-free#12044
edsiper merged 3 commits into
masterfrom
fix-12025-priority-queue-stale-event

Conversation

@edsiper

@edsiper edsiper commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #12025. The in_forward input thread could spin at 100% CPU with connections stuck in CLOSE_WAIT and their event handlers never dispatched. Root cause is in the shared downstream connection teardown path; while reproducing it I also found and fixed a related use-after-free on the pause/shutdown path.

Root cause

When a batch of connections times out in a single sweep, each gets prepare_destroy_conn() + mk_event_inject(). The event loop only processes FLB_ENGINE_LOOP_MAX_ITER (10) events per round, but flb_downstream_conn_pending_destroy_list() freed all queued connections at the end of the round — including those whose injected event was still linked in the priority bucket queue via _priority_head.

That produced two failures:

  1. 100% CPU hang (the reported issue). The freed connection left a stale, non-NULL _priority_head in the bucket queue. flb_event_load_bucket_queue_event() skips it (prev == NULL check fails), so the readable CLOSE_WAIT fd keeps waking epoll while its handler is never dispatched → the input thread spins. There is also a use-after-free when the freed node is popped on the next iteration.

  2. Pause/shutdown crash. in_forward never registered a drop-notification callback, so when the engine reclaimed a timed-out connection the plugin's fw_conn wrapper was left pointing at freed memory. The next fw_conn_del_all() (backpressure pause or shutdown) dereferenced it.

The upstream connection path already guards against (1) — see e7caf86 — but the downstream path was missing the equivalent protection.

Fix

  • flb_downstream.c (primary): defer destroying a connection while its event is still linked in the priority bucket queue; it stays in the destroy queue and is freed on a later sweep once the event has drained. Mirrors the existing upstream guard.
  • in_forward (fw_conn.c): register a drop_notification_callback so the plugin wrapper is released in sync with the connection, matching the pattern flb_http_server already uses. Splits wrapper cleanup from the downstream release to avoid double-release, and guards fw_conn_event against an injected event firing after teardown.

Reproduction

Opening many idle TLS connections so net.io_timeout expires for the whole batch in one sweep reproduces all three reported symptoms on the pre-fix build: forward thread at ~100% CPU with no clients connected, sockets in CLOSE_WAIT, and (via gdb) events with stale non-NULL _priority_head. Valgrind shows the use-after-free with the exact stack from the issue (freed in destroy_conn, read in flb_bucket_queue_pop_min from input_thread).

Testing

  • New integration regression test (test_in_forward_tls_idle_connection_timeout_churn) drives the idle-connection-timeout churn and asserts the listener survives and keeps ingesting.
  • Full lifecycle including shutdown is clean under Valgrind (0 errors), down from 26 invalid reads/writes + SIGABRT before the fix.
  • scenarios/in_forward integration suite passes.

Note: the primary change is in shared core code (flb_downstream.c) and affects all network inputs, not just forward.

Summary by CodeRabbit

  • Bug Fixes
    • Improved forward-secure connection lifetime management to avoid stale event handling and dangling references during connection churn.
    • Hardened downstream connection teardown so connections aren’t freed while still referenced by internal event queues, preventing leaks and reducing risk of instability.
  • Tests
    • Added a new integration scenario covering forward connection churn over TLS.
    • Added a regression test that repeatedly churns idle TLS sockets and confirms messages continue to forward successfully afterward.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db869d94-2b27-4308-9349-5c98cfde24d0

📥 Commits

Reviewing files that changed from the base of the PR and between 45eec99 and 7111417.

📒 Files selected for processing (4)
  • plugins/in_forward/fw_conn.c
  • src/flb_downstream.c
  • tests/integration/scenarios/in_forward/config/in_forward_conn_churn.yaml
  • tests/integration/scenarios/in_forward/tests/test_in_forward_001.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/integration/scenarios/in_forward/tests/test_in_forward_001.py
  • tests/integration/scenarios/in_forward/config/in_forward_conn_churn.yaml
  • plugins/in_forward/fw_conn.c

📝 Walkthrough

Walkthrough

This PR hardens forward connection teardown and downstream destruction handling, and adds an integration test that churns idle TLS forward connections before verifying the listener still works.

Changes

Forward connection lifecycle fix

Layer / File(s) Summary
Null-wrapper guards in event callbacks
plugins/in_forward/fw_conn.c
Adds a forward declaration for fw_conn_drop and early-return guards in fw_conn_event_internal and fw_conn_event when connection->user_data is NULL.
Wrapper teardown refactor and drop callback
plugins/in_forward/fw_conn.c
Registers fw_conn_drop as drop_notification_callback, introduces fw_conn_release() for wrapper cleanup, adds fw_conn_drop() to detach and release the wrapper, and rewrites fw_conn_del() to detach then release.
Downstream destroy_conn safety check
src/flb_downstream.c
Expands the delay-destruction condition to also check whether the connection event is still linked in the priority bucket queue, and unlinks lingering priority-queue entries during destroy-queue cleanup.
TLS idle-connection churn regression test
tests/integration/scenarios/in_forward/config/in_forward_conn_churn.yaml, tests/integration/scenarios/in_forward/tests/test_in_forward_001.py
Adds a TLS-enabled forward-input test config and a pytest test that opens many idle TLS connections, then verifies a later message still forwards successfully.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: bug, backport to v4.2.x

Suggested reviewers: cosmo0920, leonardo-albertovich

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main fix: forward input CPU hang and connection teardown use-after-free.
Linked Issues check ✅ Passed The changes address #12025 by cleaning up timed-out forward connections and preventing stale event references and teardown UAFs.
Out of Scope Changes check ✅ Passed The added downstream teardown and integration test stay aligned with the reported forward-input cleanup bug.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-12025-priority-queue-stale-event

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.

@edsiper edsiper added this to the Fluent Bit v5.0.9 milestone Jul 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/integration/scenarios/in_forward/tests/test_in_forward_001.py (1)

1030-1030: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unused monkeypatch parameter.

The test never uses monkeypatch. Consider dropping it unless a follow-up change needs it.

♻️ Proposed fix
-def test_in_forward_tls_idle_connection_timeout_churn(monkeypatch):
+def test_in_forward_tls_idle_connection_timeout_churn():
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/scenarios/in_forward/tests/test_in_forward_001.py` at line
1030, The test function test_in_forward_tls_idle_connection_timeout_churn has an
unused monkeypatch parameter, so remove it from the function signature unless it
is needed for a future fixture change. Update the test definition to match the
actual setup used in the body and keep the name-based locator unchanged so the
test remains easy to find.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/integration/scenarios/in_forward/tests/test_in_forward_001.py`:
- Line 1030: The test function test_in_forward_tls_idle_connection_timeout_churn
has an unused monkeypatch parameter, so remove it from the function signature
unless it is needed for a future fixture change. Update the test definition to
match the actual setup used in the body and keep the name-based locator
unchanged so the test remains easy to find.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 06787a48-dc46-48de-915f-2b5c537b9300

📥 Commits

Reviewing files that changed from the base of the PR and between 2aa68dd and 6d2e4a3.

📒 Files selected for processing (4)
  • plugins/in_forward/fw_conn.c
  • src/flb_downstream.c
  • tests/integration/scenarios/in_forward/config/in_forward_conn_churn.yaml
  • tests/integration/scenarios/in_forward/tests/test_in_forward_001.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d2e4a3451

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/in_forward/fw_conn.c Outdated
Comment thread src/flb_downstream.c Outdated
edsiper added 3 commits July 3, 2026 20:21
A connection destroyed by the downstream timeout/pending-destroy
machinery can still have its event linked in the priority bucket queue
after mk_event_inject (only FLB_ENGINE_LOOP_MAX_ITER events are processed
per loop round). destroy_conn freed the connection unconditionally,
leaving a dangling _priority_head in the bucket queue.

On the next loop iteration flb_bucket_queue_pop_min dereferenced the
freed node (use-after-free), and the stale self-referential linkage made
flb_event_load_bucket_queue_event skip the event forever, so a socket in
CLOSE_WAIT kept waking epoll while its handler was never dispatched,
spinning the input thread at 100% CPU.

Mirror the guard already used by upstream destroy_conn: keep the
connection in the destroy_queue until its event has drained from the
bucket queue, then free it on a later sweep.

Fixes #12025

Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
The plugin kept a fw_conn wrapper in ctx->connections for every active
connection but never registered a drop notification callback. When the
engine destroyed the underlying connection on its own (e.g. an IO
timeout via prepare_destroy_conn), the wrapper was left pointing at a
freed connection. The next fw_conn_del_all (backpressure pause or
shutdown) then dereferenced it, causing a use-after-free crash.

Register a drop_notification_callback that detaches and frees the
wrapper when the engine reclaims the connection, mirroring the
http_server pattern. Split the wrapper cleanup (fw_conn_release) from
the downstream release so fw_conn_del and the drop path don't
double-release, and guard fw_conn_event against a wrapper already
dropped by an injected event that fires after teardown.

Fixes #12025

Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
Regression test for issue #12025: open many idle TLS connections so
net.io_timeout expires for the whole batch in one downstream timeout
sweep, repeat, then verify the listener survives and still ingests. This
exercises the priority-queue teardown and the connection wrapper drop
path that previously spun the input thread at 100% CPU and crashed on
pause/shutdown.

Signed-off-by: Eduardo Silva <eduardo@chronosphere.io>
@edsiper
edsiper force-pushed the fix-12025-priority-queue-stale-event branch from 45eec99 to 7111417 Compare July 4, 2026 02:22
@edsiper
edsiper merged commit 26b8e74 into master Jul 4, 2026
85 of 89 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forward input: connection cleanup fails, hangs CPU at 100%

2 participants