Files

639 lines
18 KiB
C

/*
* device lifecycle tests.
*
* these drive hio_dev_make()/halt()/kill() through a synthetic stream
* device layered over one end of a socketpair. the socket exists only so
* the device has a real handle for the multiplexer to watch; the method
* table is a stub the test controls.
*
* lifetime bugs in an event loop normally show up as a use-after-free,
* which is a crash rather than a test failure. to turn them into ordinary
* assertions the test installs a quarantining allocator: freed blocks are
* parked instead of released, so a device object stays readable after
* hio_dev_kill() has freed it. each device carries a magic word that its
* kill method stamps dead, and every event callback checks that word
* before doing anything else. a callback reaching a killed device is then
* a plain 'not ok' instead of a segfault.
*/
#include <hio-prv.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
#include <errno.h>
#include "tap.h"
#define TDEV_LIVE 0x5A5A5A5Au
#define TDEV_DEAD 0xDEADDEADu
/* whether the multiplexer dispatches from a snapshot of events taken
* before any callback runs. epoll and kqueue fill mux->revs with raw
* device pointers and then walk it, so an entry can outlive the device it
* names. poll and select instead walk their live registration tables,
* which hio_sys_ctrlmux(DELETE) updates in place, so a killed device
* simply disappears from the walk. the same selection order as
* lib/sys-prv.h. */
#if defined(HAVE_SYS_EVENT_H) && defined(HAVE_KQUEUE) && defined(HAVE_KEVENT)
# define MUX_DISPATCHES_FROM_SNAPSHOT 1
#elif defined(HAVE_SYS_EPOLL_H)
# define MUX_DISPATCHES_FROM_SNAPSHOT 1
#else
# define MUX_DISPATCHES_FROM_SNAPSHOT 0
#endif
/* ------------------------------------------------------------------ */
/* quarantining allocator */
/* ------------------------------------------------------------------ */
#define QUARANTINE_MAX 8192
static void* g_quarantine[QUARANTINE_MAX];
static int g_quarantine_n = 0;
static int g_quarantine_overflow = 0;
static void* q_alloc (hio_mmgr_t* mmgr, hio_oow_t size) { return malloc(size); }
static void* q_realloc (hio_mmgr_t* mmgr, void* ptr, hio_oow_t size) { return realloc(ptr, size); }
static void q_free (hio_mmgr_t* mmgr, void* ptr)
{
/* park the block rather than releasing it, so a stale pointer still
* reads back the contents it had at free time */
if (g_quarantine_n < QUARANTINE_MAX) g_quarantine[g_quarantine_n++] = ptr;
else { g_quarantine_overflow++; free (ptr); }
}
static hio_mmgr_t g_qmmgr = { q_alloc, q_realloc, q_free, HIO_NULL };
static void quarantine_drain (void)
{
while (g_quarantine_n > 0) free (g_quarantine[--g_quarantine_n]);
}
/* ------------------------------------------------------------------ */
/* the synthetic device */
/* ------------------------------------------------------------------ */
typedef struct tdev_t tdev_t;
struct tdev_t
{
HIO_DEV_HEADER;
unsigned int magic;
int id;
int fd; /* the device's end of the socketpair */
/* kill behaviour knobs */
int kill_fail_left; /* fail this many non-forced kills before succeeding */
/* what the read callback should do when it next fires */
tdev_t* kill_on_read; /* if set, hio_dev_kill() this device from on_read */
};
typedef struct tdev_make_t tdev_make_t;
struct tdev_make_t
{
int id;
int fd;
int fail_make;
};
/* observations shared with the test body */
static int g_kill_calls = 0;
static int g_kill_max_force = -1;
static int g_make_calls = 0;
static int g_fail_before_make_calls = 0;
static int g_on_read_calls = 0;
static int g_on_read_eof = 0;
static int g_use_after_free = 0; /* callbacks reaching a killed device */
static hio_t* g_hio = HIO_NULL;
static void obs_reset (void)
{
g_kill_calls = 0;
g_kill_max_force = -1;
g_make_calls = 0;
g_fail_before_make_calls = 0;
g_on_read_calls = 0;
g_on_read_eof = 0;
g_use_after_free = 0;
}
/* keep the library's own debug logging out of the test output; genuine
* errors still surface on stderr where the TAP driver captures them */
static void quiet_logging (hio_t* hio)
{
hio_bitmask_t mask = HIO_LOG_ERROR | HIO_LOG_FATAL | HIO_LOG_ALL_TYPES;
hio_setoption (hio, HIO_LOG_MASK, &mask);
}
/* returns 0 if the device is still live; records a violation otherwise */
static int check_live (hio_dev_t* dev, const char* where)
{
tdev_t* t = (tdev_t*)dev;
if (t->magic != TDEV_LIVE)
{
g_use_after_free++;
diag (where);
return -1;
}
return 0;
}
static int tdev_make (hio_dev_t* dev, void* ctx)
{
tdev_t* t = (tdev_t*)dev;
tdev_make_t* mi = (tdev_make_t*)ctx;
g_make_calls++;
if (mi->fail_make)
{
hio_seterrbfmt (dev->hio, HIO_EINVAL, "make refused by test");
return -1;
}
t->magic = TDEV_LIVE;
t->id = mi->id;
t->fd = mi->fd;
t->kill_fail_left = 0;
t->kill_on_read = HIO_NULL;
t->dev_cap |= HIO_DEV_CAP_STREAM;
return 0;
}
static int tdev_kill (hio_dev_t* dev, int force)
{
tdev_t* t = (tdev_t*)dev;
g_kill_calls++;
if (force > g_kill_max_force) g_kill_max_force = force;
/* honour the contract in hio.h: only an unforced kill may refuse */
if (force == 0 && t->kill_fail_left > 0)
{
t->kill_fail_left--;
hio_seterrnum (dev->hio, HIO_EAGAIN);
return -1;
}
t->magic = TDEV_DEAD;
if (t->fd >= 0) { close (t->fd); t->fd = -1; }
return 0;
}
static void tdev_fail_before_make (void* ctx)
{
g_fail_before_make_calls++;
}
static hio_syshnd_t tdev_getsyshnd (hio_dev_t* dev)
{
return (hio_syshnd_t)((tdev_t*)dev)->fd;
}
static int tdev_read (hio_dev_t* dev, void* buf, hio_iolen_t* len, hio_devaddr_t* srcaddr)
{
tdev_t* t = (tdev_t*)dev;
ssize_t n;
if (check_live(dev, "read method on a killed device") <= -1) return -1;
n = recv(t->fd, buf, *len, 0);
if (n <= -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return 0;
hio_seterrwithsyserr (dev->hio, 0, errno);
return -1;
}
*len = n;
return 1;
}
static int tdev_write (hio_dev_t* dev, const void* data, hio_iolen_t* len, const hio_devaddr_t* dstaddr)
{
tdev_t* t = (tdev_t*)dev;
ssize_t n;
if (check_live(dev, "write method on a killed device") <= -1) return -1;
if (*len <= 0) { shutdown (t->fd, SHUT_WR); return 1; }
n = send(t->fd, data, *len, 0);
if (n <= -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return 0;
hio_seterrwithsyserr (dev->hio, 0, errno);
return -1;
}
*len = n;
return 1;
}
static int tdev_writev (hio_dev_t* dev, const hio_iovec_t* iov, hio_iolen_t* iovcnt, const hio_devaddr_t* dstaddr)
{
tdev_t* t = (tdev_t*)dev;
ssize_t n;
if (check_live(dev, "writev method on a killed device") <= -1) return -1;
n = writev(t->fd, (const struct iovec*)iov, *iovcnt);
if (n <= -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return 0;
hio_seterrwithsyserr (dev->hio, 0, errno);
return -1;
}
*iovcnt = n;
return 1;
}
static hio_dev_mth_t tdev_mth =
{
tdev_make,
tdev_kill,
tdev_fail_before_make,
tdev_getsyshnd,
HIO_NULL, /* issyshndbroken */
HIO_NULL, /* ioctl */
tdev_read,
tdev_write,
tdev_writev,
HIO_NULL /* sendfile */
};
static int tdev_ready (hio_dev_t* dev, int events)
{
/* first thing the loop touches on a dispatched device. checking the
* magic here catches a stale pointer before any further dereference,
* and returning 0 keeps the loop from attempting I/O on it. */
tdev_t* t = (tdev_t*)dev;
if (t->magic != TDEV_LIVE)
{
g_use_after_free++;
diag ("event dispatched to a device that was already killed");
return 0;
}
return 1;
}
static int tdev_on_read (hio_dev_t* dev, const void* data, hio_iolen_t len, const hio_devaddr_t* srcaddr)
{
tdev_t* t = (tdev_t*)dev;
if (check_live(dev, "on_read on a killed device") <= -1) return 0;
g_on_read_calls++;
if (len == 0) g_on_read_eof++;
if (t->kill_on_read)
{
tdev_t* victim = t->kill_on_read;
t->kill_on_read = HIO_NULL;
victim->kill_on_read = HIO_NULL; /* don't let the victim retaliate */
hio_dev_kill ((hio_dev_t*)victim);
}
return 0; /* one read per event; don't be greedy */
}
static int tdev_on_write (hio_dev_t* dev, hio_iolen_t wrlen, void* wrctx, const hio_devaddr_t* dstaddr)
{
if (check_live(dev, "on_write on a killed device") <= -1) return 0;
return 0;
}
static hio_dev_evcb_t tdev_evcb = { tdev_ready, tdev_on_read, tdev_on_write };
/* ------------------------------------------------------------------ */
/* helpers */
/* ------------------------------------------------------------------ */
static int make_pair (int* devfd, int* peerfd)
{
int sp[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp) <= -1) return -1;
hio_makesyshndasync (g_hio, sp[0]);
hio_makesyshndasync (g_hio, sp[1]);
*devfd = sp[0];
*peerfd = sp[1];
return 0;
}
static tdev_t* make_tdev (int id, int* peerfd)
{
tdev_make_t mi;
int devfd;
if (make_pair(&devfd, peerfd) <= -1) return HIO_NULL;
mi.id = id;
mi.fd = devfd;
mi.fail_make = 0;
return (tdev_t*)hio_dev_make(g_hio, HIO_SIZEOF(tdev_t), &tdev_mth, &tdev_evcb, &mi);
}
static void nop_tmr (hio_t* hio, const hio_ntime_t* now, hio_tmrjob_t* job) { }
/* run exactly one loop iteration, bounded by a short timer so an
* iteration with nothing ready cannot stall the test for a full second */
static void pump (void)
{
hio_ntime_t after;
hio_tmridx_t idx = HIO_TMRIDX_INVALID;
HIO_INIT_NTIME (&after, 0, 20000000); /* 20ms */
hio_schedtmrjobafter (g_hio, &after, nop_tmr, &idx, HIO_NULL);
hio_exec (g_hio);
if (idx != HIO_TMRIDX_INVALID) hio_deltmrjob (g_hio, idx);
}
/* ------------------------------------------------------------------ */
/* tests */
/* ------------------------------------------------------------------ */
static void test_make_and_caps (void)
{
tdev_t* dev;
int peerfd;
obs_reset ();
dev = make_tdev(1, &peerfd);
OK (dev != HIO_NULL, "hio_dev_make() succeeds");
OK (g_make_calls == 1, "the make method is called exactly once");
OK ((dev->dev_cap & HIO_DEV_CAP_ACTIVE) != 0, "a new device is marked active");
OK ((dev->dev_cap & HIO_DEV_CAP_WATCH_STARTED) != 0, "a new device is registered with the multiplexer");
OK ((dev->dev_cap & HIO_DEV_CAP_IN_WATCHED) != 0, "input watching is on by default");
OK ((dev->dev_cap & HIO_DEV_CAP_OUT_WATCHED) == 0, "output watching is off until there is something to write");
OK (g_kill_calls == 0, "the kill method is not called during make");
hio_dev_kill ((hio_dev_t*)dev);
close (peerfd);
}
static void test_make_failures (void)
{
tdev_make_t mi;
hio_dev_t* dev;
int devfd, peerfd;
/* dev_size below the header: rejected before the make method runs */
obs_reset ();
mi.id = 0; mi.fd = -1; mi.fail_make = 0;
dev = hio_dev_make(g_hio, HIO_SIZEOF(hio_dev_t) - 1, &tdev_mth, &tdev_evcb, &mi);
OK (dev == HIO_NULL, "hio_dev_make() rejects an undersized dev_size");
OK (g_fail_before_make_calls == 1, "fail_before_make is invoked when make is never reached");
OK (g_make_calls == 0, "the make method is not called for an undersized device");
OK (hio_geterrnum(g_hio) == HIO_EINVAL, "the undersized case reports HIO_EINVAL");
/* the make method itself refusing */
obs_reset ();
if (make_pair(&devfd, &peerfd) >= 0)
{
mi.id = 0; mi.fd = devfd; mi.fail_make = 1;
dev = hio_dev_make(g_hio, HIO_SIZEOF(tdev_t), &tdev_mth, &tdev_evcb, &mi);
OK (dev == HIO_NULL, "hio_dev_make() fails when the make method refuses");
OK (g_make_calls == 1, "the make method ran before refusing");
OK (g_kill_calls == 0, "the kill method is not called when make itself fails");
OK (g_fail_before_make_calls == 0, "fail_before_make is not invoked once make has run");
close (devfd);
close (peerfd);
}
else skip ("socketpair unavailable", 4);
}
static void test_halt_is_deferred (void)
{
tdev_t* dev;
int peerfd;
obs_reset ();
dev = make_tdev(2, &peerfd);
if (!dev) { skip ("device creation failed", 6); return; }
hio_dev_halt ((hio_dev_t*)dev);
OK (g_kill_calls == 0, "hio_dev_halt() does not destroy the device immediately");
OK ((dev->dev_cap & HIO_DEV_CAP_ACTIVE) == 0, "a halted device is no longer active");
OK ((dev->dev_cap & HIO_DEV_CAP_HALTED) != 0, "a halted device is marked halted");
OK (dev->magic == TDEV_LIVE, "a halted device is still live");
/* halting again must not queue a second destruction */
hio_dev_halt ((hio_dev_t*)dev);
pump ();
OK (g_kill_calls == 1, "the loop destroys a halted device exactly once");
OK (g_kill_max_force == 0, "an ordinary destruction is not forced");
close (peerfd);
}
static void test_kill_is_immediate (void)
{
tdev_t* dev;
int peerfd;
obs_reset ();
dev = make_tdev(3, &peerfd);
if (!dev) { skip ("device creation failed", 3); return; }
hio_dev_kill ((hio_dev_t*)dev);
OK (g_kill_calls == 1, "hio_dev_kill() destroys the device synchronously");
OK (dev->magic == TDEV_DEAD, "the kill method ran before hio_dev_kill() returned");
pump ();
OK (g_kill_calls == 1, "a killed device is not destroyed a second time by the loop");
close (peerfd);
}
static void test_kill_after_halt (void)
{
tdev_t* dev;
int peerfd;
obs_reset ();
dev = make_tdev(4, &peerfd);
if (!dev) { skip ("device creation failed", 2); return; }
hio_dev_halt ((hio_dev_t*)dev);
hio_dev_kill ((hio_dev_t*)dev);
OK (g_kill_calls == 1, "killing a halted device destroys it once");
pump ();
OK (g_kill_calls == 1, "the loop does not re-destroy a device killed while halted");
close (peerfd);
}
static void test_zombie_escalation (void)
{
/* a kill method that refuses turns the device into a zombie. hio_fini()
* must escalate the force argument rather than leaking or spinning. */
hio_t* hio;
hio_errinf_t errinf;
hio_t* saved = g_hio;
tdev_t* dev;
int peerfd;
obs_reset ();
hio = hio_open(&g_qmmgr, 0, HIO_NULL, HIO_FEATURE_ALL, 16, &errinf);
if (!hio) { skip ("unable to open a second hio", 4); return; }
quiet_logging (hio);
g_hio = hio;
dev = make_tdev(5, &peerfd);
if (!dev) { g_hio = saved; hio_close (hio); skip ("device creation failed", 4); return; }
dev->kill_fail_left = 99; /* refuse every unforced kill */
hio_dev_kill ((hio_dev_t*)dev);
OK (g_kill_calls == 1, "the refusing kill method was called once");
OK ((dev->dev_cap & HIO_DEV_CAP_ZOMBIE) != 0, "a device whose kill refuses becomes a zombie");
OK (dev->magic == TDEV_LIVE, "a zombie device has not been destroyed yet");
hio_close (hio);
OK (g_kill_max_force >= 1, "shutdown escalates to a forced kill rather than leaking the zombie");
g_hio = saved;
close (peerfd);
}
static void test_read_and_eof (void)
{
tdev_t* dev;
int peerfd;
obs_reset ();
dev = make_tdev(6, &peerfd);
if (!dev) { skip ("device creation failed", 5); return; }
if (write(peerfd, "hello", 5) != 5) { skip ("peer write failed", 5); goto done; }
pump ();
OK (g_on_read_calls == 1, "readable data delivers exactly one on_read");
OK (g_on_read_eof == 0, "a data read is not reported as EOF");
/* backpressure: with input watching off, pending data must not be delivered */
hio_dev_read ((hio_dev_t*)dev, 0);
if (write(peerfd, "more", 4) != 4) { skip ("peer write failed", 3); goto done; }
pump ();
OK (g_on_read_calls == 1, "no on_read is delivered while input watching is disabled");
hio_dev_read ((hio_dev_t*)dev, 1);
pump ();
OK (g_on_read_calls == 2, "re-enabling input watching delivers the buffered data");
/* EOF */
close (peerfd);
peerfd = -1;
pump ();
OK (g_on_read_eof == 1, "peer close is reported as a zero-length on_read");
done:
if (peerfd >= 0) close (peerfd);
if (dev->magic == TDEV_LIVE) hio_dev_kill ((hio_dev_t*)dev);
}
static void test_kill_peer_from_callback (void)
{
/* two devices are made readable together so the multiplexer reports
* both in one batch. whichever is dispatched first kills the other, so
* the second entry in the batch always refers to a device that is gone -
* regardless of the order the kernel chose.
*
* this only bites on a multiplexer that dispatches from a snapshot; see
* MUX_DISPATCHES_FROM_SNAPSHOT above. the core covers it by holding the
* memory of a device killed mid-dispatch until the batch finishes, and
* by skipping any event for a device that is no longer active. the test
* runs on every backend so both halves stay covered. */
tdev_t* a, * b;
int peer_a = -1, peer_b = -1;
obs_reset ();
a = make_tdev(7, &peer_a);
b = make_tdev(8, &peer_b);
if (!a || !b) { skip ("device creation failed", 2); goto done; }
a->kill_on_read = b;
b->kill_on_read = a;
if (write(peer_a, "x", 1) != 1 || write(peer_b, "y", 1) != 1)
{
skip ("peer write failed", 2);
goto done;
}
pump ();
OK (g_on_read_calls == 1, "the surviving device read once and killed its peer");
OK (g_use_after_free == 0, "no event is dispatched to a device killed earlier in the same batch");
done:
if (peer_a >= 0) close (peer_a);
if (peer_b >= 0) close (peer_b);
if (a && a->magic == TDEV_LIVE) hio_dev_kill ((hio_dev_t*)a);
if (b && b->magic == TDEV_LIVE) hio_dev_kill ((hio_dev_t*)b);
}
static void test_fini_kills_survivors (void)
{
/* devices still active or halted at hio_close() time must be destroyed */
hio_t* hio;
hio_errinf_t errinf;
hio_t* saved = g_hio;
tdev_t* live, * halted;
int pa = -1, pb = -1;
obs_reset ();
hio = hio_open(&g_qmmgr, 0, HIO_NULL, HIO_FEATURE_ALL, 16, &errinf);
if (!hio) { skip ("unable to open a second hio", 2); return; }
quiet_logging (hio);
g_hio = hio;
live = make_tdev(9, &pa);
halted = make_tdev(10, &pb);
if (halted) hio_dev_halt ((hio_dev_t*)halted);
hio_close (hio);
OK (g_kill_calls == 2, "hio_close() destroys both active and halted devices");
OK ((!live || live->magic == TDEV_DEAD) && (!halted || halted->magic == TDEV_DEAD),
"no device survives hio_close()");
g_hio = saved;
if (pa >= 0) close (pa);
if (pb >= 0) close (pb);
}
/* ------------------------------------------------------------------ */
int main (void)
{
hio_errinf_t errinf;
no_plan ();
g_hio = hio_open(&g_qmmgr, 0, HIO_NULL, HIO_FEATURE_ALL, 16, &errinf);
quiet_logging (g_hio);
if (!g_hio)
{
bail_out ("unable to open hio");
return -1;
}
test_make_and_caps ();
test_make_failures ();
test_halt_is_deferred ();
test_kill_is_immediate ();
test_kill_after_halt ();
test_zombie_escalation ();
test_read_and_eof ();
test_kill_peer_from_callback ();
test_fini_kills_survivors ();
hio_close (g_hio);
OK (g_quarantine_overflow == 0, "the quarantine held every freed block for the whole run");
quarantine_drain ();
return exit_status();
}