Skip to content

common: guard ecdh() against HSM fd being non-blocking - #9402

Draft
daywalker90 wants to merge 6 commits into
ElementsProject:masterfrom
daywalker90:fix-websocket-macos
Draft

common: guard ecdh() against HSM fd being non-blocking#9402
daywalker90 wants to merge 6 commits into
ElementsProject:masterfrom
daywalker90:fix-websocket-macos

Conversation

@daywalker90

Copy link
Copy Markdown
Collaborator

Fixes: #9323

On macOS under load, subdaemons intermittently die:

  connectd: **BROKEN** STATUS_FAIL_HSM_IO: No hsmd ECDH response
  channeld: exits 0 after WIRE_HSMD_GET_PER_COMMITMENT_POINT

Both are a synchronous wire_sync_read() returning NULL on a fresh
connection.  The HSM fd in a subdaemon is one end of a socketpair created
by hsmd (whose io loop sets O_NONBLOCK on the other end) and passed
hsmd -> lightningd -> subdaemon via SCM_RIGHTS.  On macOS the O_NONBLOCK
flag follows the shared open file description across that chain, so the
subdaemon's fd can be non-blocking, and read()/write() return EAGAIN
before hsmd's (fast) reply has landed.  Linux keeps the descriptions
independent, which is why this never reproduces there.

The previous attempts to force the fd blocking from the subdaemon
(io_fd_block in ecdh_hsmd_setup) cannot win, because hsmd owns the other
end of the same open file description and keeps it non-blocking.

Fix at the shared choke point instead: make wire_sync_read() and
wire_sync_write() tolerant of O_NONBLOCK by polling on EAGAIN and
resuming, preserving any partial read.  "Sync" then really means "read or
write a complete message", regardless of the fd's blocking state.  This
covers connectd's ecdh(), channeld's hsm_req(), and every other subdaemon
that does synchronous HSM I/O (openingd, closingd, onchaind).

Also drop the now-unneeded io_fd_block() toggling from ecdh_hsmd_setup(),
and include errno in the HSM I/O failure messages so a recurrence is
diagnosable from the daemon log.

Changelog-Fixed: connectd: fix intermittent "No hsmd ECDH response" crash on macOS under load (issue ElementsProject#9060).
The "tolerate a non-blocking fd" change to wire_sync_read() did not fix the
macOS flake; it only changed where it fails.  Instead of failing cleanly on
EAGAIN ("No hsmd ECDH response") it now read a corrupted/desynced message
and aborted (SIGABRT in fromwire_hsmd_ecdh_resp, tal "Not a valid header").
Revert wire_sync.c to upstream.

We still don't know whether the HSM fd is O_NONBLOCK (EAGAIN), sees EOF (hsmd
closed the socketpair), or delivers garbage, so make the failure diagnosable:
- report errno in the "Write ECDH to hsmd failed" and "No hsmd ECDH response"
  messages, and
- if hsmd's reply won't parse, hex-dump the exact bytes we read before
  failing.

These are temporary diagnostics to pin down the macOS-only flake and should
be removed once root-caused.
channeld and openingd intermittently die on the macOS CI flake
(test_websocket), e.g. "channeld died (0)" from hsm_req()'s
wire_sync_read() returning NULL, and "openingd died" from its sync HSM I/O.
connectd's ECDH is immune because ecdh_hsmd_setup() makes the HSM fd
blocking.

On macOS the O_NONBLOCK flag of the HSM socketpair is shared across the
SCM_RIGHTS chain with hsmd's io loop, so the subdaemon's end can be
O_NONBLOCK and a synchronous read() spuriously EAGAINs.  Explicitly assert
the fd is blocking at the top of each sync-HSM subdaemon's main(), before
any wire_sync HSM I/O.  On Linux the fd is already blocking, so this is a
no-op (it mirrors subd.c:read_fds() and ecdh_hsmd_setup()).

Changelog-Fixed: closingd: fix intermittent death from non-blocking HSM fd on macOS
The macOS CI flake manifests as subdaemons dying on synchronous HSM I/O:
connectd "No hsmd ECDH response: Undefined error: 0" (errno 0 = EOF), and
channeld/openingd/simpleclosed dying in hsm_req().  Making the HSM fd
blocking (io_fd_block) was proven ineffective, so this adds diagnostics to
pin down where the EOF comes from:

- ecdh_hsmd: report the fd number alongside errno on sync failure, and drop
  the io_fd_block() call (it contends over the shared open file description
  and is not the fix).
- hsmd: log "Destroying client N" in destroy_client(), so we can tell
  whether hsmd is dropping a subdaemon's HSM client (subdaemon then reads
  EOF) versus the subdaemon's fd being aliased.
- channeld/simpleclosed hsm_req: report errno and fd on failure.

All temporary diagnostics to be removed once root-caused.
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.

CI flake test_websocket on macos

1 participant