Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b5a5acb
ccan/io: retain registrations during poll dispatch
niftynei Aug 25, 2026
aedb145
ccan/io: test fd replacement during poll dispatch
niftynei Aug 24, 2026
aedac15
tests: add regression test for #8902 dual-fund disconnect
vincenzopalazzo Mar 25, 2026
556a082
dualopend: Parse error messages returned from lightningd correctly
niftynei Aug 8, 2026
392c363
funder: remove listener for peer disconnects
niftynei Aug 8, 2026
79bfe7c
dualopend: free penalty base on commit failure
niftynei Aug 22, 2026
d470471
dualopend: report HSM transport failures explicitly
niftynei Aug 22, 2026
eb39e18
lightningd: retire openingd before starting channeld
niftynei Aug 22, 2026
5a86382
dualopend: delay re-start of dualopend after an abort to prevent HSM …
niftynei Aug 9, 2026
9670b19
connectd: acknowledge replacement subdaemon routes
niftynei Aug 22, 2026
05456c2
dualopend: resume saved opens across disconnects
niftynei Aug 25, 2026
9f269ac
tests: cover half-saved dual-open reconnection
niftynei Aug 22, 2026
fc568f2
tests: wait for dual-open disconnect before restart
niftynei Aug 22, 2026
a1dfd12
tests: retry zeroconf reconnect after route failure
niftynei Aug 22, 2026
2d01ee8
tests: wait for injected commitment disconnect
niftynei Aug 22, 2026
9e8b736
tests: expect dual-open RPCs to survive reconnects
niftynei Aug 22, 2026
8fb55e2
tests: exercise RBF reconnect wire faults independently
niftynei Aug 22, 2026
91170e3
tests: cover repeated dual-open owner generations
niftynei Aug 22, 2026
2aa996b
tests: wait for RBF reconnect transitions to complete
niftynei Aug 22, 2026
17b7f8d
tests: shorten dual-open disconnect timeout
niftynei Aug 22, 2026
7d35f5a
dualopend: force-close funding confirmed without peer signatures
niftynei Aug 25, 2026
275456d
tests: wait for RBF transactions to be broadcast explicitly
niftynei Aug 9, 2026
2e8c30a
tests: retain signed inflight after dual-open abort
niftynei Aug 22, 2026
e3b68ef
tests: sync mined RBF inflight before reconnecting
niftynei Aug 22, 2026
889bb39
tests: shorten channel lease expiry test
niftynei Aug 26, 2026
eacf896
dualopend: wait for terminal initial-open teardown
niftynei Aug 31, 2026
44ffc89
dualopend: allocate queryrates transport before channel
niftynei Aug 31, 2026
736c78e
tests: cover repeated initial-open abort generations
niftynei Aug 31, 2026
f73319f
tests: retry after dual-open hook rejection
niftynei Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ccan/ccan/io/backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ struct fd {
/* We could put these in io_plan, but they pack nicely here */
bool exclusive[2];
size_t backend_info;
/* Stable while a poll result still refers to this registration. */
struct io_fd_registration *registration;
};

/* Listeners create connections. */
Expand Down
5 changes: 5 additions & 0 deletions ccan/ccan/io/io.h
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,11 @@ struct timemono (*io_time_override(struct timemono (*now)(void)))(void);
*/
int (*io_poll_override(int (*poll)(struct pollfd *fds, nfds_t nfds, int timeout)))(struct pollfd *, nfds_t, int);

/* Protect daemons which replace connections inside readiness callbacks from
* delivering an old poll result to a newly registered fd object. */
void io_poll_protect_stale_fds(void);


/**
* io_have_fd - do we own this file descriptor?
* @fd: the file descriptor.
Expand Down
169 changes: 168 additions & 1 deletion ccan/ccan/io/poll.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,69 @@
#include <ccan/time/time.h>
#include <ccan/timer/timer.h>

struct io_fd_registration {
struct fd *fd;
size_t refs;
};

struct ready_fd {
/* NULL is the position at which to service an IO_ALWAYS plan. */
struct io_fd_registration *registration;
short revents;
};

static size_t num_fds = 0, max_fds = 0, num_waiting = 0, num_always = 0, max_always = 0, num_exclusive = 0;
static struct pollfd *pollfds = NULL;
static struct fd **fds = NULL;
static bool protect_stale_fds = false;
static struct ready_fd *ready_fds = NULL;
static size_t ready_fds_capacity = 0;
static struct io_plan **always = NULL;
static struct timemono (*nowfn)(void) = time_mono;
static int (*pollfn)(struct pollfd *fds, nfds_t nfds, int timeout) = poll;

static void cleanup_ready_fds(void)
{
free(ready_fds);
ready_fds = NULL;
ready_fds_capacity = 0;
}

static struct io_fd_registration *new_registration(struct fd *fd)
{
struct io_fd_registration *registration = malloc(sizeof(*registration));

if (!registration)
return NULL;
registration->fd = fd;
registration->refs = 1;
return registration;
}

static void registration_put(struct io_fd_registration *registration)
{
assert(registration->refs != 0);
registration->refs--;
if (registration->refs == 0)
free(registration);
}

void io_poll_protect_stale_fds(void)
{
if (protect_stale_fds)
return;
if (atexit(cleanup_ready_fds) != 0)
abort();

/* This can be enabled after callers have already registered fds. */
for (size_t i = 0; i < num_fds; i++) {
fds[i]->registration = new_registration(fds[i]);
if (!fds[i]->registration)
abort();
}
protect_stale_fds = true;
}

struct timemono (*io_time_override(struct timemono (*now)(void)))(void)
{
struct timemono (*old)(void) = nowfn;
Expand Down Expand Up @@ -54,6 +110,12 @@ static bool add_fd(struct fd *fd, short events)
return false;
max_fds = num;
}
if (protect_stale_fds) {
fd->registration = new_registration(fd);
if (!fd->registration)
return false;
} else
fd->registration = NULL;

pollfds[num_fds].events = events;
/* In case it's idle. */
Expand All @@ -75,6 +137,7 @@ static bool add_fd(struct fd *fd, short events)
static void del_fd(struct fd *fd)
{
size_t n = fd->backend_info;
struct io_fd_registration *registration = fd->registration;

assert(n != -1);
assert(n < num_fds);
Expand All @@ -98,6 +161,12 @@ static void del_fd(struct fd *fd)
}
num_fds--;
fd->backend_info = -1;
if (registration) {
assert(registration->fd == fd);
registration->fd = NULL;
fd->registration = NULL;
registration_put(registration);
}

if (fd->exclusive[IO_IN])
num_exclusive--;
Expand Down Expand Up @@ -369,6 +438,26 @@ static void restore_pollfds(void)
}
}

static void append_ready_fd(size_t *num_ready,
struct io_fd_registration *registration,
short revents)
{
if (*num_ready == ready_fds_capacity) {
size_t capacity = ready_fds_capacity ? ready_fds_capacity * 2 : 8;
struct ready_fd *new_ready_fds;

new_ready_fds = realloc(ready_fds,
sizeof(*ready_fds) * capacity);
if (!new_ready_fds)
abort();
ready_fds = new_ready_fds;
ready_fds_capacity = capacity;
}
ready_fds[*num_ready].registration = registration;
ready_fds[*num_ready].revents = revents;
(*num_ready)++;
}

/* This is the main loop. */
void *io_loop(struct timers *timers, struct timer **expired)
{
Expand Down Expand Up @@ -431,6 +520,85 @@ void *io_loop(struct timers *timers, struct timer **expired)
break;
}

if (protect_stale_fds) {
size_t num_polled = num_fds;
size_t num_ready = 0;

if (r == 0) {
handle_always();
continue;
}

/* Preserve the old fairness order, but retain only ready fds and
* the point where IO_ALWAYS work was interleaved. */
fairness_counter++;
for (size_t rotation = 0; rotation < num_polled; rotation++) {
struct io_fd_registration *registration;

i = (rotation + fairness_counter) % num_polled;
if (i == 0)
append_ready_fd(&num_ready, NULL, 0);

if (!pollfds[i].revents)
continue;
registration = fds[i]->registration;
assert(registration);
registration->refs++;
append_ready_fd(&num_ready, registration,
pollfds[i].revents);
}
for (size_t n = 0; n < num_polled; n++)
pollfds[n].revents = 0;

for (size_t n = 0; n < num_ready && !io_loop_return; n++) {
socklen_t errno_len = sizeof(errno);
struct io_fd_registration *registration;
struct fd *fd;
struct io_conn *c;
int events;

registration = ready_fds[n].registration;
if (!registration) {
if (handle_always())
break;
continue;
}
fd = registration->fd;
if (!fd)
continue;
events = ready_fds[n].revents;
c = (void *)fd;
if (fd->listener) {
struct io_listener *l = (void *)fd;
if (events & POLLIN) {
accept_conn(l);
r--;
} else if (events & (POLLHUP|POLLNVAL|POLLERR)) {
r--;
errno = EBADF;
io_close_listener(l);
}
} else if (events & (POLLIN|POLLOUT)) {
r--;
io_ready(c, events);
} else if (events & (POLLHUP|POLLNVAL|POLLERR)) {
r--;
if (getsockopt(fd->fd, SOL_SOCKET,
SO_ERROR, &errno,
&errno_len) == -1)
errno = EBADF;
io_close(c);
}
}
/* Callbacks can retire registrations, so release snapshot
* references only after dispatch has stopped using the buffer. */
for (size_t n = 0; n < num_ready; n++) {
if (ready_fds[n].registration)
registration_put(ready_fds[n].registration);
}
continue;
}

fairness_counter++;
for (size_t rotation = 0; rotation < num_fds && !io_loop_return; rotation++) {
socklen_t errno_len = sizeof(errno);
Expand Down Expand Up @@ -486,7 +654,6 @@ void *io_loop(struct timers *timers, struct timer **expired)
}
}
}

ret = io_loop_return;
io_loop_return = NULL;

Expand Down
142 changes: 142 additions & 0 deletions ccan/ccan/io/test/run-49-stale-fd-readiness.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#include "config.h"
#include <assert.h>
#include <ccan/io/io.h>
/* Include the C files directly to make each readiness delivery observable. */
#include <ccan/io/poll.c>
#include <ccan/io/io.c>
#include <unistd.h>

static int replacement_fd;
static unsigned int poll_calls, actor_events, victim_events,
survivor_events, replacement_events;
static char completed;

static void check_ready_fds_cleaned(void)
{
assert(ready_fds == NULL);
assert(ready_fds_capacity == 0);
}

static int replacement_event(int fd, struct io_plan_arg *arg)
{
replacement_events++;
return 1;
}

static struct io_plan *replacement_ready(struct io_conn *conn, void *unused)
{
io_break(&completed);
return io_close(conn);
}

static struct io_plan *replacement_init(struct io_conn *conn, void *unused)
{
io_plan_arg(conn, IO_IN);
return io_set_plan(conn, IO_IN, replacement_event,
replacement_ready, NULL);
}

static int actor_event(int fd, struct io_plan_arg *arg)
{
actor_events++;
return 1;
}

static struct io_plan *actor_ready(struct io_conn *conn, void *unused)
{
struct io_plan *closed = io_close(conn);

/* Closing the current fd compacts the table. Adding its replacement
* restores the old table length, but must not make it part of the poll
* result currently being dispatched. */
if (!io_new_conn(NULL, replacement_fd, replacement_init, NULL))
abort();
return closed;
}

static struct io_plan *actor_init(struct io_conn *conn, void *unused)
{
io_plan_arg(conn, IO_IN);
return io_set_plan(conn, IO_IN, actor_event, actor_ready, NULL);
}

static int victim_event(int fd, struct io_plan_arg *arg)
{
victim_events++;
return 1;
}

static struct io_plan *victim_init(struct io_conn *conn, void *unused)
{
io_plan_arg(conn, IO_IN);
return io_set_plan(conn, IO_IN, victim_event, io_close_cb, NULL);
}

static int survivor_event(int fd, struct io_plan_arg *arg)
{
survivor_events++;
return 1;
}

static struct io_plan *survivor_init(struct io_conn *conn, void *unused)
{
io_plan_arg(conn, IO_IN);
return io_set_plan(conn, IO_IN, survivor_event, io_close_cb, NULL);
}

static int fake_poll(struct pollfd *fds, nfds_t nfds, int timeout)
{
poll_calls++;
for (size_t i = 0; i < nfds; i++)
fds[i].revents = 0;

if (poll_calls == 1) {
/* Fairness rotation handles slot 1 first. That actor replaces
* itself while slots 0 and 2 still have readiness pending. */
assert(nfds == 3);
for (size_t i = 0; i < nfds; i++)
fds[i].revents = POLLIN;
return 3;
}

assert(poll_calls == 2);
for (size_t i = 0; i < nfds; i++) {
if (fds[i].fd != replacement_fd)
continue;
fds[i].revents = POLLIN;
return 1;
}
abort();
}

int main(void)
{
int actor_pipe[2], victim_pipe[2], survivor_pipe[2], replacement_pipe[2];

assert(pipe(actor_pipe) == 0);
assert(pipe(victim_pipe) == 0);
assert(pipe(survivor_pipe) == 0);
assert(pipe(replacement_pipe) == 0);
replacement_fd = replacement_pipe[0];

assert(io_poll_override(fake_poll) == poll);
assert(io_new_conn(NULL, victim_pipe[0], victim_init, NULL));
assert(io_new_conn(NULL, actor_pipe[0], actor_init, NULL));
assert(io_new_conn(NULL, survivor_pipe[0], survivor_init, NULL));

/* Registered first so this runs after the backend's LIFO cleanup. */
assert(atexit(check_ready_fds_cleaned) == 0);
io_poll_protect_stale_fds();
assert(io_loop(NULL, NULL) == &completed);
assert(poll_calls == 2);
assert(actor_events == 1);
assert(victim_events == 1);
assert(survivor_events == 1);
assert(replacement_events == 1);

close(actor_pipe[1]);
close(victim_pipe[1]);
close(survivor_pipe[1]);
close(replacement_pipe[1]);
return 0;
}
Loading
Loading