Skip to content

plugin: don't panic on BrokenPipe when lightningd has died#146

Merged
vincenzopalazzo merged 1 commit into
laanwj:masterfrom
vincenzopalazzo:plugin/no-broken-pipe-panic
May 6, 2026
Merged

plugin: don't panic on BrokenPipe when lightningd has died#146
vincenzopalazzo merged 1 commit into
laanwj:masterfrom
vincenzopalazzo:plugin/no-broken-pipe-panic

Conversation

@vincenzopalazzo

Copy link
Copy Markdown
Collaborator

Summary

  • Plugin::log() and the response-writing path in Plugin::start() both call .unwrap() on writes to stdout. When lightningd exits or closes the pipe between the plugin reading a request and writing back, the unwrap panics with BrokenPipe instead of letting the plugin exit cleanly.
  • Make both writers best-effort: in log() we swallow the error (logs are lossy by design), and in start() we break out of the read loop so the plugin terminates cleanly. On a healthy parent the behavior is unchanged — the subsequent read_line would have returned Ok(0) and broken the loop anyway.

Background

Observed in the wild while running folgore under CLN v25.09.3 on a low-RAM host. When lightningd died for any reason (fatal signal, OOM kill), one or more folgore_plugin Rust processes were left as orphans, spinning on stdin in an empty read loop or — worse — having panicked on a half-written response and exited mid-state.

The EOF check on read_line already added in 4f8df9a covers the "parent closed our stdin" case, but only after a clean iteration. If the parent dies between request and response, the unwrap fires first.

After applying both fixes locally and verifying via [patch.crates-io] against the actual folgore_plugin binary across multiple lightningd death cycles: zero orphan plugin processes, clean exits every time.

Test plan

  • cargo build --release -p clightningrpc-plugin
  • Existing test suite passes (cargo test)
  • Manual: run any plugin built against this branch, kill lightningd with SIGKILL, confirm the plugin exits within one stdin read cycle rather than panicking or orphaning

🤖 Generated with Claude Code

Two paths in the plugin runtime previously called `.unwrap()` on writes
to stdout, which panic with BrokenPipe if lightningd has exited or
closed the pipe ahead of the plugin:

  - `Plugin::log()` writes a JSON-RPC log notification on every log line
  - `Plugin::start()` writes the response to each handled RPC/hook call

When lightningd dies (e.g. cleanly during shutdown, or unexpectedly via
a fatal signal), all of its plugins should also exit cleanly — but a
panic in either of these paths leaves a half-dead Rust plugin that has
already lost its parent. In practice this surfaced as orphan plugin
processes in the host's process table after lightningd crashed,
spinning on the now-empty stdin until something else cleaned them up.

The EOF check on `read_line` already added in 4f8df9a handles the
"parent closed our stdin" case, but it only kicks in *after* we
successfully wrote a response. If the parent dies between us reading
its request and writing back, the unwrap fires before we ever loop.

Make both writers best-effort: on `log()` swallow the error (logs are
inherently lossy and we don't want a stray log call to take down the
plugin), and in the RPC response path break out of the read loop so
the plugin terminates cleanly rather than panicking. The subsequent
`read_line` would have returned `Ok(0)` anyway, so the behavior on a
healthy parent is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vincenzopalazzo vincenzopalazzo merged commit 586f4f4 into laanwj:master May 6, 2026
4 checks passed
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.

1 participant