connectd: keep the subd fd until connectd has received it
What changed, and why it matters
This commit fixes a macOS-specific bug where opening a Lightning channel could fail or hang with 'Peer connection lost'. The root cause was a race condition in how file descriptors (sockets) were passed between internal processes: the sender closed its copy too early, before the receiver had fully received it. The fix makes the sender wait for an acknowledgment before closing its copy. It is a reliability/availability fix, not an exploitable security vulnerability.
Treat as a normal bug fix. No security-specific action required beyond standard review and regression testing on macOS for channel open paths.
Security signals we found
Race condition in inter-process file descriptor passing (SCM_RIGHTS)
Availability impact: fundchannel failure/hang under load on macOS
No evidence of confidentiality or integrity compromise
Fix adds explicit acknowledgment before releasing fd reference
Evidence from the diff
The patch addresses an SCM_RIGHTS file-descriptor passing race on macOS. Previously, lightningd queued a peer socket fd for connectd via subd_send_fd and immediately closed its own copy. On macOS, if the sending fd is closed before recvmsg completes, the received fd can become unreadable. The fix introduces a new wire message, connectd_peer_connect_subd_reply, sent by connectd after it has received the fd. lightningd now duplicates the fd, sends the duplicate via subd_req, and keeps the original until the reply arrives, at which point the tal destructor frees it. The helper connectd_connect_subd centralizes this pattern across channel_control, dual_open_control, opening_control, and peer_control.
Changed components
connectd/connectd.cconnectd/connectd_wire.csvlightningd/channel_control.clightningd/connect_control.clightningd/connect_control.hlightningd/dual_open_control.clightningd/opening_control.clightningd/peer_control.cInspect captured patch +65 / −53
### connectd/connectd.c
@@ -2363,6 +2363,8 @@ static struct io_plan *recv_peer_connect_subd(struct io_conn *conn,
int fd,
struct daemon *daemon)
{
+ daemon_conn_send(daemon->master,
+ take(towire_connectd_peer_connect_subd_reply(NULL)));
peer_connect_subd(daemon, msg, fd);
return daemon_conn_read_next(conn, daemon->master);
}
@@ -2470,6 +2472,7 @@ static struct io_plan *recv_req(struct io_conn *conn,
/* We send these, we don't receive them */
case WIRE_CONNECTD_INIT_REPLY:
case WIRE_CONNECTD_ACTIVATE_REPLY:
+ case WIRE_CONNECTD_PEER_CONNECT_SUBD_REPLY:
case WIRE_CONNECTD_PEER_CONNECTED:
case WIRE_CONNECTD_PEER_SPOKE:
case WIRE_CONNECTD_CONNECT_FAILED:
### connectd/connectd_wire.csv
@@ -112,6 +112,9 @@ msgdata,connectd_peer_connect_subd,id,node_id,
msgdata,connectd_peer_connect_subd,counter,u64,
msgdata,connectd_peer_connect_subd,channel_id,channel_id,
+# Connectd -> master: got the fd, you can close your copy.
+msgtype,connectd_peer_connect_subd_reply,2104
+
# Connectd -> master: peer said something interesting
msgtype,connectd_peer_spoke,2005
msgdata,connectd_peer_spoke,id,node_id,
### lightningd/channel_control.c
@@ -395,12 +395,7 @@ static void handle_splice_abort(struct lightningd *ld,
}
if (peer_start_channeld(channel, pfd, NULL, false)) {
- subd_send_msg(ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL,
- &peer->id,
- peer->connectd_counter,
- &channel->cid)));
- subd_send_fd(ld->connectd, other_fd);
+ connectd_connect_subd(peer, &channel->cid, other_fd);
} else {
log_info(channel->log, "peer_start_channeld failed");
close(other_fd);
### lightningd/connect_control.c
@@ -4,6 +4,7 @@
#include <ccan/tal/str/str.h>
#include <common/json_command.h>
#include <connectd/connectd_wiregen.h>
+#include <errno.h>
#include <hsmd/permissions.h>
#include <lightningd/channel.h>
#include <lightningd/connect_control.h>
@@ -12,8 +13,10 @@
#include <lightningd/notification.h>
#include <lightningd/onion_message.h>
#include <lightningd/opening_common.h>
+#include <lightningd/peer_fd.h>
#include <lightningd/ping.h>
#include <lightningd/plugin_hook.h>
+#include <unistd.h>
struct connect {
struct list_node list;
@@ -490,6 +493,37 @@ void connectd_connect_to_peer(struct lightningd *ld,
reason)));
}
+/* Freeing the request frees our copy of the fd. */
+static void peer_connect_subd_reply(struct subd *connectd UNUSED,
+ const u8 *reply,
+ const int *fds UNUSED,
+ struct peer_fd *our_copy UNUSED)
+{
+ if (!fromwire_connectd_peer_connect_subd_reply(reply))
+ fatal("Bad connectd_peer_connect_subd_reply: %s",
+ tal_hex(reply, reply));
+}
+
+/*~ On macOS, a socket whose only reference is in flight over SCM_RIGHTS can
+ * arrive unable to read. So connectd gets a dup, and we keep ours until it
+ * replies. */
+void connectd_connect_subd(const struct peer *peer,
+ const struct channel_id *channel_id,
+ int fd)
+{
+ int copy = dup(fd);
+
+ if (copy < 0)
+ fatal("Could not dup fd for connectd: %s", strerror(errno));
+
+ subd_req(peer->ld->connectd, peer->ld->connectd,
+ take(towire_connectd_peer_connect_subd(NULL, &peer->id,
+ peer->connectd_counter,
+ channel_id)),
+ copy, 0, peer_connect_subd_reply,
+ take(new_peer_fd(NULL, fd)));
+}
+
void tell_connectd_peer_importance(struct peer *peer,
bool was_important)
{
@@ -561,6 +595,7 @@ static unsigned connectd_msg(struct subd *connectd, const u8 *msg, const int *fd
/* This is a reply, so never gets through to here. */
case WIRE_CONNECTD_INIT_REPLY:
case WIRE_CONNECTD_ACTIVATE_REPLY:
+ case WIRE_CONNECTD_PEER_CONNECT_SUBD_REPLY:
case WIRE_CONNECTD_DEV_MEMLEAK_REPLY:
case WIRE_CONNECTD_START_SHUTDOWN_REPLY:
case WIRE_CONNECTD_INJECT_ONIONMSG_REPLY:
### lightningd/connect_control.h
@@ -5,6 +5,7 @@
#include <ccan/tal/tal.h>
#include <common/utils.h>
+struct channel_id;
struct lightningd;
struct peer;
struct pubkey;
@@ -21,6 +22,12 @@ void connectd_connect_to_peer(struct lightningd *ld,
const char *reason,
bool is_important);
+/* Tell connectd to connect this channel to the subd at the other end of fd
+ * (takes ownership of fd). */
+void connectd_connect_subd(const struct peer *peer,
+ const struct channel_id *channel_id,
+ int fd);
+
/* Kill subds, tell connectd to disconnect once they're drained. */
void force_peer_disconnect(struct lightningd *ld,
const struct peer *peer,
### lightningd/dual_open_control.c
@@ -2670,12 +2670,7 @@ static char *restart_dualopend(const tal_t *ctx, const struct lightningd *ld,
close(other_fd);
return tal_fmt(ctx, "Peer not connected");
}
- subd_send_msg(ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL,
- &channel->peer->id,
- channel->peer->connectd_counter,
- &channel->cid)));
- subd_send_fd(ld->connectd, other_fd);
+ connectd_connect_subd(channel->peer, &channel->cid, other_fd);
return NULL;
}
@@ -3378,12 +3373,7 @@ static struct command_result *openchannel_init(struct command *cmd,
subd_send_msg(channel->owner, channel->open_attempt->open_msg);
/* Tell connectd connect this to this channel id. */
- subd_send_msg(peer->ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL,
- &peer->id,
- peer->connectd_counter,
- &channel->cid)));
- subd_send_fd(peer->ld->connectd, fds[1]);
+ connectd_connect_subd(peer, &channel->cid, fds[1]);
return command_still_pending(cmd);
}
@@ -4255,12 +4245,7 @@ static struct command_result *json_queryrates(struct command *cmd,
subd_send_msg(channel->owner, channel->open_attempt->open_msg);
/* Tell connectd connect this to this channel id. */
- subd_send_msg(peer->ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL,
- &peer->id,
- peer->connectd_counter,
- &channel->cid)));
- subd_send_fd(peer->ld->connectd, fds[1]);
+ connectd_connect_subd(peer, &channel->cid, fds[1]);
return command_still_pending(cmd);
}
### lightningd/opening_control.c
@@ -1249,12 +1249,7 @@ static struct command_result *fundchannel_start(struct command *cmd,
subd_send_msg(peer->uncommitted_channel->open_daemon, fc->open_msg);
/* Tell connectd connect this to this channel id. */
- subd_send_msg(peer->ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL,
- &peer->id,
- peer->connectd_counter,
- &peer->uncommitted_channel->cid)));
- subd_send_fd(peer->ld->connectd, fds[1]);
+ connectd_connect_subd(peer, &peer->uncommitted_channel->cid, fds[1]);
return command_still_pending(cmd);
}
### lightningd/peer_control.c
@@ -1497,12 +1497,7 @@ static void connect_activate_subd(struct lightningd *ld, struct channel *channel
abort();
tell_connectd:
- subd_send_msg(ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL,
- &channel->peer->id,
- channel->peer->connectd_counter,
- &channel->cid)));
- subd_send_fd(ld->connectd, other_fd);
+ connectd_connect_subd(channel->peer, &channel->cid, other_fd);
return;
send_error:
@@ -2383,11 +2378,7 @@ void handle_peer_spoke(struct lightningd *ld, const u8 *msg)
return;
tell_connectd:
- subd_send_msg(ld->connectd,
- take(towire_connectd_peer_connect_subd(NULL, &id,
- peer->connectd_counter,
- &channel_id)));
- subd_send_fd(ld->connectd, other_fd);
+ connectd_connect_subd(peer, &channel_id, other_fd);
}
struct disconnect_command {
### lightningd/test/run-invoice-select-inchan.c
@@ -223,6 +223,11 @@ void connect_succeeded(struct lightningd *ld UNNEEDED, const struct peer *peer U
bool incoming UNNEEDED,
const struct wireaddr_internal *addr UNNEEDED)
{ fprintf(stderr, "connect_succeeded called!\n"); abort(); }
+/* Generated stub for connectd_connect_subd */
+void connectd_connect_subd(const struct peer *peer UNNEEDED,
+ const struct channel_id *channel_id UNNEEDED,
+ int fd UNNEEDED)
+{ fprintf(stderr, "connectd_connect_subd called!\n"); abort(); }
/* Generated stub for connectd_connect_to_peer */
void connectd_connect_to_peer(struct lightningd *ld UNNEEDED,
const struct peer *peer UNNEEDED,
@@ -618,9 +623,6 @@ struct subd_req *subd_req_(const tal_t *ctx UNNEEDED,
void (*replycb)(struct subd * UNNEEDED, const u8 * UNNEEDED, const int * UNNEEDED, void *) UNNEEDED,
void *replycb_data TAKES UNNEEDED)
{ fprintf(stderr, "subd_req_ called!\n"); abort(); }
-/* Generated stub for subd_send_fd */
-void subd_send_fd(struct subd *sd UNNEEDED, int fd UNNEEDED)
-{ fprintf(stderr, "subd_send_fd called!\n"); abort(); }
/* Generated stub for subd_send_msg */
void subd_send_msg(struct subd *sd UNNEEDED, const u8 *msg_out UNNEEDED)
{ fprintf(stderr, "subd_send_msg called!\n"); abort(); }
@@ -633,9 +635,6 @@ u8 *towire_channeld_dev_reenable_commit(const tal_t *ctx UNNEEDED)
/* Generated stub for towire_connectd_disconnect_peer */
u8 *towire_connectd_disconnect_peer(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED)
{ fprintf(stderr, "towire_connectd_disconnect_peer called!\n"); abort(); }
-/* Generated stub for towire_connectd_peer_connect_subd */
-u8 *towire_connectd_peer_connect_subd(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, const struct channel_id *channel_id UNNEEDED)
-{ fprintf(stderr, "towire_connectd_peer_connect_subd called!\n"); abort(); }
/* Generated stub for towire_connectd_peer_send_msg */
u8 *towire_connectd_peer_send_msg(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, const u8 *msg UNNEEDED)
{ fprintf(stderr, "towire_connectd_peer_send_msg called!\n"); abort(); }
### wallet/test/run-wallet.c
@@ -222,6 +222,11 @@ void connect_succeeded(struct lightningd *ld UNNEEDED, const struct peer *peer U
bool incoming UNNEEDED,
const struct wireaddr_internal *addr UNNEEDED)
{ fprintf(stderr, "connect_succeeded called!\n"); abort(); }
+/* Generated stub for connectd_connect_subd */
+void connectd_connect_subd(const struct peer *peer UNNEEDED,
+ const struct channel_id *channel_id UNNEEDED,
+ int fd UNNEEDED)
+{ fprintf(stderr, "connectd_connect_subd called!\n"); abort(); }
/* Generated stub for connectd_connect_to_peer */
void connectd_connect_to_peer(struct lightningd *ld UNNEEDED,
const struct peer *peer UNNEEDED,
@@ -652,9 +657,6 @@ struct subd_req *subd_req_(const tal_t *ctx UNNEEDED,
void (*replycb)(struct subd * UNNEEDED, const u8 * UNNEEDED, const int * UNNEEDED, void *) UNNEEDED,
void *replycb_data TAKES UNNEEDED)
{ fprintf(stderr, "subd_req_ called!\n"); abort(); }
-/* Generated stub for subd_send_fd */
-void subd_send_fd(struct subd *sd UNNEEDED, int fd UNNEEDED)
-{ fprintf(stderr, "subd_send_fd called!\n"); abort(); }
/* Generated stub for subd_send_msg */
void subd_send_msg(struct subd *sd UNNEEDED, const u8 *msg_out UNNEEDED)
{ fprintf(stderr, "subd_send_msg called!\n"); abort(); }
@@ -702,9 +704,6 @@ u8 *towire_channeld_sending_commitsig_reply(const tal_t *ctx UNNEEDED)
/* Generated stub for towire_connectd_disconnect_peer */
u8 *towire_connectd_disconnect_peer(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED)
{ fprintf(stderr, "towire_connectd_disconnect_peer called!\n"); abort(); }
-/* Generated stub for towire_connectd_peer_connect_subd */
-u8 *towire_connectd_peer_connect_subd(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, const struct channel_id *channel_id UNNEEDED)
-{ fprintf(stderr, "towire_connectd_peer_connect_subd called!\n"); abort(); }
/* Generated stub for towire_connectd_peer_send_msg */
u8 *towire_connectd_peer_send_msg(const tal_t *ctx UNNEEDED, const struct node_id *id UNNEEDED, u64 counter UNNEEDED, const u8 *msg UNNEEDED)
{ fprintf(stderr, "towire_connectd_peer_send_msg called!\n"); abort(); }Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.