From e363491ea7ba4352d89e00a2a0672727d42916e4 Mon Sep 17 00:00:00 2001 From: Trond Norbye Date: Thu, 2 Jul 2026 12:17:39 +0200 Subject: [PATCH 1/4] test: fix OpenSSL 3.0 test suite compatibility issues Under OpenSSL 3.0+, unexpected socket closures before a clean SSL/TLS shutdown alert is exchanged are classified as protocol errors rather than clean EOFs, causing regression tests to fail. - https_bev: enable allow_dirty_shutdown on server bufferevents to handle abrupt client socket closures cleanly - http_incomplete_errorcb: accept BEV_EVENT_ERROR with an OpenSSL error as a successful termination (OpenSSL 3.0 treats raw socket shutdowns as TLS protocol errors) --- test/regress_http.c | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/test/regress_http.c b/test/regress_http.c index 6056e3967..f1da7ff18 100644 --- a/test/regress_http.c +++ b/test/regress_http.c @@ -121,13 +121,20 @@ static struct bufferevent * https_bev(struct event_base *base, void *arg) { SSL *ssl = SSL_new(get_ssl_ctx()); + struct bufferevent *bev; SSL_use_certificate(ssl, ssl_getcert(ssl_getkey())); SSL_use_PrivateKey(ssl, ssl_getkey()); - return bufferevent_openssl_socket_new( + bev = bufferevent_openssl_socket_new( base, -1, ssl, BUFFEREVENT_SSL_ACCEPTING, BEV_OPT_CLOSE_ON_FREE); + if (!bev) { + SSL_free(ssl); + return NULL; + } + bufferevent_openssl_set_allow_dirty_shutdown(bev, 1); + return bev; } #endif static struct evhttp * @@ -3078,10 +3085,19 @@ http_incomplete_errorcb(struct bufferevent *bev, short what, void *arg) if (what & BEV_EVENT_CONNECTED) return; - if (what == (BEV_EVENT_READING|BEV_EVENT_EOF)) + if (what == (BEV_EVENT_READING|BEV_EVENT_EOF)) { test_ok++; - else + } else if (what == (BEV_EVENT_READING|BEV_EVENT_ERROR)) { + /* Under SSL, raw socket shutdowns trigger TLS alert protocol errors on OpenSSL 3.0. + * We accept this as a successful termination for this incomplete request test. */ + if (bufferevent_get_openssl_error(bev) != 0) { + test_ok++; + } else { + test_ok = -2; + } + } else { test_ok = -2; + } event_base_loopexit(exit_base,NULL); } From 50068cb6280b826827b8ecd87ef853307d00d9aa Mon Sep 17 00:00:00 2001 From: Trond Norbye Date: Thu, 2 Jul 2026 12:22:15 +0200 Subject: [PATCH 2/4] Add kernel socket receive timestamp support Implement kernel-measured socket receive timestamps with nanosecond precision on supported platforms via the recvmsg() syscall. Each recvmsg() call writes into a freshly allocated evbuffer chain so that every call gets an independent timestamp; timestamps are never silently discarded due to chain reuse. Infrastructure changes: - evbuffer-internal.h: Add timestamp storage and validity flag to evbuffer_chain structure. - evbuffer_read_with_timestamp(): New internal function for reading data via recvmsg(), parsing control messages for timestamps, and safeguarding against MSG_CTRUNC truncation. Always allocates a fresh chain per call so per-call timestamps are preserved independently. - bufferevent-internal.h: Add recv_timestamps_enabled flag to the bufferevent_private structure. - bufferevent_sock.c: Enable receive timestamps and swap standard reads with evbuffer_read_with_timestamp() when requested. - bufferevent_openssl.c: Add a custom socket BIO to extract kernel timestamps during direct socket reads, and propagate timestamps from underlying bufferevents in filtered TLS mode. - Build system: Add CMake and Autotools socket timestamp checks for SO_TIMESTAMP and SO_TIMESTAMPNS option availability. Public API additions: - BEV_OPT_RECV_TIMESTAMPS option flag for socket bufferevents. - evbuffer_get_timestamp() to fetch the receipt timestamp of the oldest data currently in the buffer. - evbuffer_commit_space_with_timespec() to commit reserved space and manually attach a timestamp to the committed chains. Conflict resolution: preserved the upstream UBSAN fix (if (chain->buffer) guard in evbuffer_pullup) while adding timestamp propagation alongside it. --- CMakeLists.txt | 2 + buffer.c | 257 ++++++++++++++++++++--- bufferevent-internal.h | 6 + bufferevent_openssl.c | 396 +++++++++++++++++++++++++++++++++-- bufferevent_sock.c | 61 +++++- configure.ac | 7 + evbuffer-internal.h | 9 + event-config.h.cmake | 9 + include/event2/buffer.h | 54 +++++ include/event2/bufferevent.h | 14 +- test/regress_buffer.c | 140 ++++++++++++- test/regress_bufferevent.c | 117 +++++++++++ test/regress_ssl.c | 242 +++++++++++++++++++++ 13 files changed, 1266 insertions(+), 48 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 142d91392..5f7b23c44 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -582,6 +582,8 @@ CHECK_SYMBOL_EXISTS(TAILQ_FOREACH sys/queue.h EVENT__HAVE_TAILQFOREACH) CHECK_CONST_EXISTS(CTL_KERN sys/sysctl.h EVENT__HAVE_DECL_CTL_KERN) CHECK_CONST_EXISTS(KERN_ARND sys/sysctl.h EVENT__HAVE_DECL_KERN_ARND) CHECK_SYMBOL_EXISTS(F_SETFD fcntl.h EVENT__HAVE_SETFD) +CHECK_CONST_EXISTS(SO_TIMESTAMP sys/socket.h EVENT__HAVE_DECL_SO_TIMESTAMP) +CHECK_CONST_EXISTS(SO_TIMESTAMPNS sys/socket.h EVENT__HAVE_DECL_SO_TIMESTAMPNS) CHECK_TYPE_SIZE(fd_mask EVENT__HAVE_FD_MASK) diff --git a/buffer.c b/buffer.c index f68f31c3e..2d054f25e 100644 --- a/buffer.c +++ b/buffer.c @@ -723,6 +723,14 @@ advance_last_with_data(struct evbuffer *buf) int evbuffer_commit_space(struct evbuffer *buf, struct evbuffer_iovec *vec, int n_vecs) +{ + return evbuffer_commit_space_with_timespec(buf, vec, n_vecs, NULL); +} + +int +evbuffer_commit_space_with_timespec(struct evbuffer *buf, + struct evbuffer_iovec *vec, int n_vecs, + const struct timespec *ts) { struct evbuffer_chain *chain, **firstchainp, **chainp; int result = -1; @@ -744,6 +752,10 @@ evbuffer_commit_space(struct evbuffer *buf, goto done; buf->last->off += vec[0].iov_len; added = vec[0].iov_len; + if (ts && added && buf->last->timestamp.valid == 0) { + buf->last->timestamp.ts = *ts; + buf->last->timestamp.valid = 1; + } if (added) advance_last_with_data(buf); goto okay; @@ -773,6 +785,10 @@ evbuffer_commit_space(struct evbuffer *buf, for (i=0; ioff += vec[i].iov_len; added += vec[i].iov_len; + if (ts && vec[i].iov_len && (*chainp)->timestamp.valid == 0) { + (*chainp)->timestamp.ts = *ts; + (*chainp)->timestamp.valid = 1; + } if (vec[i].iov_len) { buf->last_with_datap = chainp; } @@ -940,6 +956,7 @@ APPEND_CHAIN_MULTICAST(struct evbuffer *dst, struct evbuffer *src) tmp->off = chain->off; tmp->flags |= EVBUFFER_MULTICAST|EVBUFFER_IMMUTABLE; tmp->buffer = chain->buffer; + tmp->timestamp = chain->timestamp; evbuffer_chain_insert(dst, tmp); } } @@ -1150,6 +1167,7 @@ evbuffer_drain(struct evbuffer *buf, size_t len) EVUTIL_ASSERT(remaining == 0); chain->misalign += chain->off; chain->off = 0; + chain->timestamp.valid = 0; break; } else evbuffer_chain_free(chain); @@ -1391,6 +1409,11 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) } if (CHAIN_PINNED(chain)) { + /* Pinned chain case: expand in-place by appending data from + * subsequent chains. Timestamps from subsequent chains being + * consolidated are intentionally discarded; only this chain's + * timestamp is preserved as it contains the oldest data. + */ size_t old_off = chain->off; if (CHAIN_SPACE_LEN(chain) < size - chain->off) { /* not enough room at end of chunk. */ @@ -1402,6 +1425,11 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) size -= old_off; chain = chain->next; } else if (chain->buffer_len - chain->misalign >= (size_t)size) { + /* Sufficient space case: expand in-place without reallocation + * by appending data from subsequent chains. Timestamps from + * subsequent chains being consolidated are intentionally discarded; + * only this chain's timestamp is preserved. + */ /* already have enough space in the first chain */ size_t old_off = chain->off; buffer = chain->buffer + chain->misalign + chain->off; @@ -1416,16 +1444,25 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) } buffer = tmp->buffer; tmp->off = size; + if (chain->timestamp.valid) { + tmp->timestamp = chain->timestamp; + } buf->first = tmp; } /* TODO(niels): deal with buffers that point to NULL like sendfile */ - /* Copy and free every chunk that will be entirely pulled into tmp */ + /* Copy and free every chunk that will be entirely pulled into tmp. + * If the destination chain still has no timestamp, pick up the first + * valid one found among the chains being consumed. */ last_with_data = *buf->last_with_datap; for (; chain != NULL && (size_t)size >= chain->off; chain = next) { next = chain->next; + if (!tmp->timestamp.valid && chain->timestamp.valid) { + tmp->timestamp = chain->timestamp; + } + if (chain->buffer) { memcpy(buffer, chain->buffer + chain->misalign, chain->off); size -= chain->off; @@ -1440,6 +1477,9 @@ evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size) } if (chain != NULL) { + if (!tmp->timestamp.valid && chain->timestamp.valid) { + tmp->timestamp = chain->timestamp; + } memcpy(buffer, chain->buffer + chain->misalign, size); chain->misalign += size; chain->off -= size; @@ -2265,8 +2305,18 @@ evbuffer_read_setup_vecs_(struct evbuffer *buf, ev_ssize_t howmuch, } static int -get_n_bytes_readable_on_socket(evutil_socket_t fd) +get_n_bytes_readable_on_socket(evutil_socket_t fd, int use_recvmsg) { +#if !defined(_WIN32) && !defined(__APPLE__) + if (use_recvmsg) { + int r = (int)recv(fd, NULL, 0, MSG_PEEK | MSG_TRUNC); + if (r > 0) { + return r; + } + return EVBUFFER_MAX_READ; + } +#endif + #if defined(FIONREAD) && defined(_WIN32) unsigned long lng = EVBUFFER_MAX_READ; if (ioctlsocket(fd, FIONREAD, &lng) < 0) @@ -2274,19 +2324,41 @@ get_n_bytes_readable_on_socket(evutil_socket_t fd) /* Can overflow, but mostly harmlessly. XXXX */ return (int)lng; #elif defined(FIONREAD) - int n = EVBUFFER_MAX_READ; - if (ioctl(fd, FIONREAD, &n) < 0) - return -1; - return n; + { + int n = EVBUFFER_MAX_READ; + if (ioctl(fd, FIONREAD, &n) < 0) + return -1; + return n; + } #else return EVBUFFER_MAX_READ; #endif } -/* TODO(niels): should this function return ev_ssize_t and take ev_ssize_t - * as howmuch? */ -int -evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +/** + * Reads data from a socket optionally with kernel timestamp support. + * + * @param buf the evbuffer to populate + * @param fd the file descriptor to use + * @param howmuch the amount of data to read (this will be adjusted; + * see below) + * @param use_recvmsg try to use recvmsg to read the data (and try to + * read kernel timestamps) + * + * If howmuch is less than 0 or greater than EVBUFFER_MAX_READ we'll try + * to read EVBUFFER_MAX_READ bytes (there may be less available on the + *socket) + * + * If use_recvmsg is nonzero, it attempts to retrieve the SO_TIMESTAMPNS or + * SO_TIMESTAMP ancillary data from recvmsg() and associate it with the buffer + * data. Each recvmsg() call writes into a newly allocated chain, so every + * call gets its own chain and its timestamp is preserved independently. + * + * TODO(niels): should this function return ev_ssize_t and take ev_ssize_t + * as howmuch? + */ +static int +evbuffer_read_impl_(struct evbuffer *buf, evutil_socket_t fd, int howmuch, int use_recvmsg) { struct evbuffer_chain **chainp; int n; @@ -2298,6 +2370,9 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) struct evbuffer_chain *chain; unsigned char *p; #endif + struct timespec ts; + int ts_found = 0; + memset(&ts, 0, sizeof(ts)); EVBUFFER_LOCK(buf); @@ -2306,33 +2381,57 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) goto done; } - n = get_n_bytes_readable_on_socket(fd); - if (n <= 0 || n > EVBUFFER_MAX_READ) + n = get_n_bytes_readable_on_socket(fd, use_recvmsg); + if (n <= 0 || (n > EVBUFFER_MAX_READ && !use_recvmsg)) n = EVBUFFER_MAX_READ; +#if !defined(_WIN32) && !defined(__APPLE__) + if (use_recvmsg && n > howmuch) { + howmuch = n; + } +#endif if (howmuch < 0 || howmuch > n) howmuch = n; #ifdef USE_IOVEC_IMPL - /* Since we can use iovecs, we're willing to use the last - * NUM_READ_IOVEC chains. */ - if (evbuffer_expand_fast_(buf, howmuch, NUM_READ_IOVEC) == -1) { - result = -1; - goto done; - } else { + { IOV_TYPE vecs[NUM_READ_IOVEC]; +#ifndef _WIN32 + if (use_recvmsg) { + /* Allocate a fresh chain for each recvmsg() call so that + * every call gets its own chain with an independent timestamp. */ + struct evbuffer_chain *old_last = buf->last; + struct evbuffer_chain *new_chain = evbuffer_chain_new(howmuch); + if (!new_chain) { + result = -1; + goto done; + } + evbuffer_chain_insert(buf, new_chain); + chainp = old_last ? &old_last->next : &buf->first; + nvecs = 1; + vecs[0].iov_base = (void *)CHAIN_SPACE_PTR(new_chain); + vecs[0].iov_len = (size_t)howmuch; + } else +#endif + /* Since we can use iovecs, we're willing to use the last + * NUM_READ_IOVEC chains. */ + if (evbuffer_expand_fast_(buf, howmuch, NUM_READ_IOVEC) == -1) { + result = -1; + goto done; + } else { #ifdef EVBUFFER_IOVEC_IS_NATIVE_ - nvecs = evbuffer_read_setup_vecs_(buf, howmuch, vecs, - NUM_READ_IOVEC, &chainp, 1); + nvecs = evbuffer_read_setup_vecs_(buf, howmuch, vecs, + NUM_READ_IOVEC, &chainp, 1); #else - /* We aren't using the native struct iovec. Therefore, - we are on win32. */ - struct evbuffer_iovec ev_vecs[NUM_READ_IOVEC]; - nvecs = evbuffer_read_setup_vecs_(buf, howmuch, ev_vecs, 2, - &chainp, 1); - - for (i=0; i < nvecs; ++i) - WSABUF_FROM_EVBUFFER_IOV(&vecs[i], &ev_vecs[i]); + /* We aren't using the native struct iovec. Therefore, + we are on win32. */ + struct evbuffer_iovec ev_vecs[NUM_READ_IOVEC]; + nvecs = evbuffer_read_setup_vecs_(buf, howmuch, ev_vecs, 2, + &chainp, 1); + + for (i=0; i < nvecs; ++i) + WSABUF_FROM_EVBUFFER_IOV(&vecs[i], &ev_vecs[i]); #endif + } #ifdef _WIN32 { @@ -2349,7 +2448,68 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) n = bytesRead; } #else - n = readv(fd, vecs, nvecs); + if (use_recvmsg) { + struct msghdr msg; + /* Control message buffer for cmsg data. + * Sized to accommodate timestamp messages (SCM_TIMESTAMPNS, + * SCM_TIMESTAMP) plus additional space (256 bytes) for other + * possible cmsg entries (SCM_RIGHTS, SCM_CREDENTIALS, etc.) + * to prevent truncation via MSG_CTRUNC which would silently + * lose timestamp information. */ +#define EVBUFFER_RECVMSG_CTRLFN_SZ \ + (CMSG_SPACE(sizeof(struct timespec)) + \ + CMSG_SPACE(sizeof(struct timeval)) + 256) + unsigned char control[EVBUFFER_RECVMSG_CTRLFN_SZ]; +#undef EVBUFFER_RECVMSG_CTRLFN_SZ + + /* Setup message header */ + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = vecs; + msg.msg_iovlen = nvecs; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + /* Receive with ancillary data */ + n = recvmsg(fd, &msg, 0); + + if (n > 0) { + /* Check if control data was truncated */ + if (!(msg.msg_flags & MSG_CTRUNC)) { + struct cmsghdr *cmsg; + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET) { + continue; + } +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (cmsg->cmsg_type == SCM_TIMESTAMPNS) { + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timespec))) { + continue; + } + ts = *(struct timespec *)CMSG_DATA(cmsg); + ts_found = 1; + break; + } +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMP + if (cmsg->cmsg_type == SCM_TIMESTAMP) { + struct timeval *tv; + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timeval))) { + continue; + } + tv = (struct timeval *)CMSG_DATA(cmsg); + ts.tv_sec = tv->tv_sec; + ts.tv_nsec = tv->tv_usec * 1000L; + ts_found = 1; + break; + } +#endif + } + } + } + } else { + n = readv(fd, vecs, nvecs); + } #endif } @@ -2394,8 +2554,16 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) if ((ev_ssize_t)space < remaining) { (*chainp)->off += space; remaining -= (int)space; + if (ts_found && (*chainp)->timestamp.valid == 0) { + (*chainp)->timestamp.ts = ts; + (*chainp)->timestamp.valid = 1; + } } else { (*chainp)->off += remaining; + if (ts_found && (*chainp)->timestamp.valid == 0) { + (*chainp)->timestamp.ts = ts; + (*chainp)->timestamp.valid = 1; + } buf->last_with_datap = chainp; break; } @@ -2416,6 +2584,37 @@ evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) return result; } +int +evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + return evbuffer_read_impl_(buf, fd, howmuch, 0); +} + +int +evbuffer_read_with_timestamp( + struct evbuffer *buf, evutil_socket_t fd, int howmuch) +{ + return evbuffer_read_impl_(buf, fd, howmuch, 1); +} + +int evbuffer_get_timestamp( + struct evbuffer *buf, struct timespec *timestamp) +{ + int result = -1; + if (!timestamp) { + return -1; + } + EVBUFFER_LOCK(buf); + { + if (buf->first && buf->first->timestamp.valid) { + *timestamp = buf->first->timestamp.ts; + result = 0; + } + } + EVBUFFER_UNLOCK(buf); + return result; +} + #ifdef USE_IOVEC_IMPL static inline int evbuffer_write_iovec(struct evbuffer *buffer, evutil_socket_t fd, diff --git a/bufferevent-internal.h b/bufferevent-internal.h index 20a8a324b..f57074bb8 100644 --- a/bufferevent-internal.h +++ b/bufferevent-internal.h @@ -231,6 +231,9 @@ struct bufferevent_private { struct sockaddr_storage conn_address; struct evdns_getaddrinfo_request *dns_request; + + /** Flag: set if receive timestamps are enabled */ + unsigned recv_timestamps_enabled : 1; }; /** Possible operations for a control callback. */ @@ -453,6 +456,9 @@ EVENT2_EXPORT_SYMBOL int bufferevent_socket_set_conn_address_(struct bufferevent *bev, struct sockaddr *addr, size_t addrlen); +EVENT2_EXPORT_SYMBOL +int be_socket_enable_timestamps_(evutil_socket_t fd); + /** Internal use: We have just successfully read data into an inbuf, so * reset the read timeout (if any). */ diff --git a/bufferevent_openssl.c b/bufferevent_openssl.c index 520e2d6ff..025544a81 100644 --- a/bufferevent_openssl.c +++ b/bufferevent_openssl.c @@ -53,6 +53,16 @@ #include #endif +#ifdef EVENT__HAVE_SYS_SOCKET_H +#include +#endif +#ifdef EVENT__HAVE_SYS_UIO_H +#include +#endif +#ifdef EVENT__HAVE_NETINET_IN_H +#include +#endif + #include "event2/bufferevent.h" #include "event2/bufferevent_struct.h" #include "event2/bufferevent_ssl.h" @@ -83,6 +93,7 @@ /* every BIO type needs its own integer type value. */ #define BIO_TYPE_LIBEVENT 57 +#define BIO_TYPE_LIBEVENT_RECVMSG (58 | BIO_TYPE_SOURCE_SINK) /* ???? Arguably, we should set BIO_TYPE_FILTER or BIO_TYPE_SOURCE_SINK on * this. */ @@ -229,21 +240,27 @@ bio_bufferevent_puts(BIO *b, const char *s) /* Method table for the bufferevent BIO */ static BIO_METHOD *methods_bufferevent; +static void +init_methods_bufferevent(void) +{ + methods_bufferevent = BIO_meth_new(BIO_TYPE_LIBEVENT, "bufferevent"); + if (methods_bufferevent == NULL) { + return; + } + BIO_meth_set_write(methods_bufferevent, bio_bufferevent_write); + BIO_meth_set_read(methods_bufferevent, bio_bufferevent_read); + BIO_meth_set_puts(methods_bufferevent, bio_bufferevent_puts); + BIO_meth_set_ctrl(methods_bufferevent, bio_bufferevent_ctrl); + BIO_meth_set_create(methods_bufferevent, bio_bufferevent_new); + BIO_meth_set_destroy(methods_bufferevent, bio_bufferevent_free); +} + /* Return the method table for the bufferevents BIO */ static BIO_METHOD * BIO_s_bufferevent(void) { - if (methods_bufferevent == NULL) { - methods_bufferevent = BIO_meth_new(BIO_TYPE_LIBEVENT, "bufferevent"); - if (methods_bufferevent == NULL) - return NULL; - BIO_meth_set_write(methods_bufferevent, bio_bufferevent_write); - BIO_meth_set_read(methods_bufferevent, bio_bufferevent_read); - BIO_meth_set_puts(methods_bufferevent, bio_bufferevent_puts); - BIO_meth_set_ctrl(methods_bufferevent, bio_bufferevent_ctrl); - BIO_meth_set_create(methods_bufferevent, bio_bufferevent_new); - BIO_meth_set_destroy(methods_bufferevent, bio_bufferevent_free); - } + static CRYPTO_ONCE once = CRYPTO_ONCE_STATIC_INIT; + CRYPTO_THREAD_run_once(&once, init_methods_bufferevent); return methods_bufferevent; } @@ -328,6 +345,235 @@ struct bufferevent_openssl { unsigned old_state : 2; }; +struct bio_socket_recvmsg_data { + evutil_socket_t fd; + struct bufferevent_openssl *bev_ssl; + struct timespec last_recv_ts; + int last_recv_ts_valid; +}; + +static int +bio_socket_recvmsg_new(BIO *b) +{ + struct bio_socket_recvmsg_data *data = mm_calloc(1, sizeof(*data)); + if (!data) { + return 0; + } + data->fd = -1; + data->bev_ssl = NULL; + data->last_recv_ts_valid = 0; + BIO_set_init(b, 1); + BIO_set_data(b, data); + return 1; +} + +static int +bio_socket_recvmsg_free(BIO *b) +{ + struct bio_socket_recvmsg_data *data; + if (!b) { + return 0; + } + data = BIO_get_data(b); + if (data) { + if (BIO_get_shutdown(b) && data->fd != EVUTIL_INVALID_SOCKET) { + evutil_closesocket(data->fd); + } + mm_free(data); + BIO_set_data(b, NULL); + } + BIO_set_init(b, 0); + return 1; +} + +static int +bio_socket_recvmsg_read(BIO *b, char *out, int outlen) +{ + struct bio_socket_recvmsg_data *data = BIO_get_data(b); + int r; + if (!data || data->fd < 0) { + return -1; + } + + BIO_clear_retry_flags(b); + +#if defined(_WIN32) + r = recv(data->fd, out, outlen, 0); +#else + if (data->bev_ssl && data->bev_ssl->bev.recv_timestamps_enabled) { + struct msghdr msg; + struct iovec iov; + unsigned char control[ + CMSG_SPACE(sizeof(struct timespec)) + /* SCM_TIMESTAMPNS */ + CMSG_SPACE(sizeof(struct timeval)) + 256 /* SCM_TIMESTAMP + extra */ + ]; + struct timespec ts; + int ts_found = 0; + + memset(&ts, 0, sizeof(ts)); + iov.iov_base = out; + iov.iov_len = outlen; + + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + r = recvmsg(data->fd, &msg, 0); + + if (r > 0) { + if (!(msg.msg_flags & MSG_CTRUNC)) { + struct cmsghdr *cmsg; + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != SOL_SOCKET) { + continue; + } +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + if (cmsg->cmsg_type == SCM_TIMESTAMPNS) { + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timespec))) { + continue; + } + ts = *(struct timespec *)CMSG_DATA(cmsg); + ts_found = 1; + break; + } +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMP + if (cmsg->cmsg_type == SCM_TIMESTAMP) { + struct timeval *tv; + if (cmsg->cmsg_len < CMSG_LEN(sizeof(struct timeval))) { + continue; + } + tv = (struct timeval *)CMSG_DATA(cmsg); + ts.tv_sec = tv->tv_sec; + ts.tv_nsec = tv->tv_usec * 1000L; + ts_found = 1; + break; + } +#endif + } + } + if (ts_found) { + data->last_recv_ts = ts; + data->last_recv_ts_valid = 1; + } + } + } else { + r = recv(data->fd, out, outlen, 0); + } +#endif + + if (r < 0) { + int err = EVUTIL_SOCKET_ERROR(); + if (EVUTIL_ERR_RW_RETRIABLE(err)) { + BIO_set_retry_read(b); + } + } + return r; +} + +static int +bio_socket_recvmsg_write(BIO *b, const char *in, int inlen) +{ + struct bio_socket_recvmsg_data *data = BIO_get_data(b); + int r; + if (!data || data->fd < 0) { + return -1; + } + + BIO_clear_retry_flags(b); + r = send(data->fd, in, inlen, 0); + if (r < 0) { + int err = EVUTIL_SOCKET_ERROR(); + if (EVUTIL_ERR_RW_RETRIABLE(err)) { + BIO_set_retry_write(b); + } + } + return r; +} + +static long +bio_socket_recvmsg_ctrl(BIO *b, int cmd, long num, void *ptr) +{ + struct bio_socket_recvmsg_data *data = BIO_get_data(b); + long ret = 1; + if (!data) { + return 0; + } + + switch (cmd) { + case BIO_C_SET_FD: + data->fd = (evutil_socket_t)(ev_intptr_t)ptr; + BIO_set_shutdown(b, (int)num); + BIO_set_init(b, 1); + ret = 1; + break; + case BIO_C_GET_FD: + if (BIO_get_init(b)) { + if (ptr) { + *(evutil_socket_t *)ptr = data->fd; + } + ret = data->fd; + } else { + ret = -1; + } + break; + case BIO_CTRL_GET_CLOSE: + ret = BIO_get_shutdown(b); + break; + case BIO_CTRL_SET_CLOSE: + BIO_set_shutdown(b, (int)num); + ret = 1; + break; + case BIO_CTRL_DUP: + case BIO_CTRL_FLUSH: + ret = 1; + break; + default: + ret = 0; + break; + } + return ret; +} + +static BIO_METHOD *methods_socket_recvmsg; + +static void +init_methods_socket_recvmsg(void) +{ + methods_socket_recvmsg = + BIO_meth_new(BIO_TYPE_LIBEVENT_RECVMSG, "socket_recvmsg"); + if (methods_socket_recvmsg == NULL) { + return; + } + BIO_meth_set_write(methods_socket_recvmsg, bio_socket_recvmsg_write); + BIO_meth_set_read(methods_socket_recvmsg, bio_socket_recvmsg_read); + BIO_meth_set_ctrl(methods_socket_recvmsg, bio_socket_recvmsg_ctrl); + BIO_meth_set_create(methods_socket_recvmsg, bio_socket_recvmsg_new); + BIO_meth_set_destroy(methods_socket_recvmsg, bio_socket_recvmsg_free); +} + +static BIO_METHOD * +BIO_s_socket_recvmsg(void) +{ + static CRYPTO_ONCE once = CRYPTO_ONCE_STATIC_INIT; + CRYPTO_THREAD_run_once(&once, init_methods_socket_recvmsg); + return methods_socket_recvmsg; +} + +static BIO * +BIO_new_socket_recvmsg(evutil_socket_t fd, int close_flag) +{ + BIO *bio = BIO_new(BIO_s_socket_recvmsg()); + if (!bio) { + return NULL; + } + BIO_ctrl(bio, BIO_C_SET_FD, close_flag, (void *)(ev_intptr_t)fd); + return bio; +} + static int be_openssl_enable(struct bufferevent *, short); static int be_openssl_disable(struct bufferevent *, short); static void be_openssl_unlink(struct bufferevent *); @@ -591,9 +837,20 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { struct evbuffer_iovec space[2]; int result = 0; + struct bio_socket_recvmsg_data *bio_data = NULL; + struct timespec first_ts = {0, 0}; + int first_ts_valid = 0; + if (bev_ssl->bev.read_suspended) return 0; + { + BIO *rbio = SSL_get_rbio(bev_ssl->ssl); + if (rbio && BIO_method_type(rbio) == BIO_TYPE_LIBEVENT_RECVMSG) { + bio_data = BIO_get_data(rbio); + } + } + atmost = bufferevent_get_read_max_(&bev_ssl->bev); if (n_to_read > atmost) n_to_read = atmost; @@ -603,8 +860,17 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { return OP_ERR; for (i=0; ibev.read_suspended) break; + if (bev_ssl->underlying) { + if (evbuffer_get_timestamp( + bufferevent_get_input(bev_ssl->underlying), + &underlying_ts) == 0) { + underlying_ts_valid = 1; + } + } ERR_clear_error(); r = SSL_read(bev_ssl->ssl, space[i].iov_base, space[i].iov_len); if (r>0) { @@ -615,6 +881,19 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { ++n_used; space[i].iov_len = r; decrement_buckets(bev_ssl); + + /* Store timestamp from first successful read (oldest data). + All iovecs from the same reserve_space call are committed + together, so we use the timestamp from the first read. */ + if (n_used == 1) { + if (bio_data && bio_data->last_recv_ts_valid) { + first_ts = bio_data->last_recv_ts; + first_ts_valid = 1; + } else if (underlying_ts_valid) { + first_ts = underlying_ts; + first_ts_valid = 1; + } + } } else { int err = SSL_get_error(bev_ssl->ssl, r); print_err(err); @@ -641,8 +920,16 @@ do_read(struct bufferevent_openssl *bev_ssl, int n_to_read) { } } + /* Commit all filled iovecs together to avoid data corruption when + evbuffer_reserve_space() returns multiple vectors. Individual commits + can cause buffer accounting issues when the second vector is in a + different chain than the first. */ if (n_used) { - evbuffer_commit_space(input, space, n_used); + if (first_ts_valid) { + evbuffer_commit_space_with_timespec(input, space, n_used, &first_ts); + } else { + evbuffer_commit_space(input, space, n_used); + } if (bev_ssl->underlying) BEV_RESET_GENERIC_READ_TIMEOUT(bev); } @@ -1232,6 +1519,16 @@ be_openssl_destruct(struct bufferevent *bev) { struct bufferevent_openssl *bev_ssl = upcast(bev); + if (!bev_ssl->underlying) { + BIO *rbio = SSL_get_rbio(bev_ssl->ssl); + if (rbio && BIO_method_type(rbio) == BIO_TYPE_LIBEVENT_RECVMSG) { + struct bio_socket_recvmsg_data *bio_data = BIO_get_data(rbio); + if (bio_data) { + bio_data->bev_ssl = NULL; + } + } + } + if (bev_ssl->bev.options & BEV_OPT_CLOSE_ON_FREE) { if (! bev_ssl->underlying) { evutil_socket_t fd = EVUTIL_INVALID_SOCKET; @@ -1306,8 +1603,28 @@ be_openssl_ctrl(struct bufferevent *bev, case BEV_CTRL_SET_FD: if (!bev_ssl->underlying) { BIO *bio; - bio = BIO_new_socket((int)data->fd, 0); + if (bev_ssl->bev.options & BEV_OPT_RECV_TIMESTAMPS) { + if (be_socket_enable_timestamps_(data->fd) >= 0) { + bev_ssl->bev.recv_timestamps_enabled = 1; + } else { + bev_ssl->bev.recv_timestamps_enabled = 0; + } + } + if (bev_ssl->bev.recv_timestamps_enabled) { + bio = BIO_new_socket_recvmsg((int)data->fd, 0); + } else { + bio = BIO_new_socket((int)data->fd, 0); + } + if (!bio) { + return -1; + } SSL_set_bio(bev_ssl->ssl, bio, bio); + if (bio && BIO_method_type(bio) == BIO_TYPE_LIBEVENT_RECVMSG) { + struct bio_socket_recvmsg_data *bio_data = BIO_get_data(bio); + if (bio_data) { + bio_data->bev_ssl = bev_ssl; + } + } } else { BIO *bio; if (!(bio = BIO_new_bufferevent(bev_ssl->underlying))) @@ -1393,6 +1710,22 @@ bufferevent_openssl_new_impl(struct event_base *base, if (be_openssl_set_fd(bev_ssl, state, fd)) goto err; + if ((options & BEV_OPT_RECV_TIMESTAMPS) && fd >= 0) { + if (be_socket_enable_timestamps_(fd) >= 0) { + bev_ssl->bev.recv_timestamps_enabled = 1; + } + } + + { + BIO *rbio = SSL_get_rbio(ssl); + if (rbio && BIO_method_type(rbio) == BIO_TYPE_LIBEVENT_RECVMSG) { + struct bio_socket_recvmsg_data *bio_data = BIO_get_data(rbio); + if (bio_data) { + bio_data->bev_ssl = bev_ssl; + } + } + } + if (underlying) { bufferevent_setwatermark(underlying, EV_READ, 0, 0); bufferevent_enable(underlying, EV_READ|EV_WRITE); @@ -1449,6 +1782,7 @@ bufferevent_openssl_socket_new(struct event_base *base, /* Does the SSL already have an fd? */ BIO *bio = SSL_get_wbio(ssl); long have_fd = -1; + int recv_timestamps_enabled = 0; if (bio) have_fd = BIO_get_fd(bio, NULL); @@ -1465,12 +1799,44 @@ bufferevent_openssl_socket_new(struct event_base *base, This is probably an error on our part. Fail. */ goto err; } - BIO_set_close(bio, 0); + /* If the existing BIO is the standard socket BIO, replace it with our custom one to support timestamps. */ + if ((options & BEV_OPT_RECV_TIMESTAMPS) && fd >= 0) { + if (be_socket_enable_timestamps_(fd) >= 0) { + recv_timestamps_enabled = 1; + } + } + if (recv_timestamps_enabled && BIO_method_type(bio) == BIO_TYPE_SOCKET) { + int close_flag = BIO_get_close(bio); + BIO *new_bio = BIO_new_socket_recvmsg((int)fd, close_flag); + if (new_bio) { + /* Prevent the old BIO from closing fd when SSL_set_bio frees it; + * new_bio now owns the fd with the original close_flag. */ + BIO_set_close(bio, BIO_NOCLOSE); + SSL_set_bio(ssl, new_bio, new_bio); + bio = new_bio; + } + } + if (BIO_method_type(bio) == BIO_TYPE_SOCKET || + BIO_method_type(bio) == BIO_TYPE_LIBEVENT_RECVMSG) { + BIO_set_close(bio, 0); + } } else { /* The SSL isn't configured with a BIO with an fd. */ if (fd >= 0) { /* ... and we have an fd we want to use. */ - bio = BIO_new_socket((int)fd, 0); + if (options & BEV_OPT_RECV_TIMESTAMPS) { + if (be_socket_enable_timestamps_(fd) >= 0) { + recv_timestamps_enabled = 1; + } + } + if (recv_timestamps_enabled) { + bio = BIO_new_socket_recvmsg((int)fd, 0); + } else { + bio = BIO_new_socket((int)fd, 0); + } + if (!bio) { + goto err; + } SSL_set_bio(ssl, bio, bio); } else { /* Leave the fd unset. */ diff --git a/bufferevent_sock.c b/bufferevent_sock.c index 543fce4bb..97f595ab0 100644 --- a/bufferevent_sock.c +++ b/bufferevent_sock.c @@ -84,6 +84,40 @@ static int be_socket_ctrl(struct bufferevent *, enum bufferevent_ctrl_op, union static void be_socket_setfd(struct bufferevent *, evutil_socket_t); +/* ======================================================================== + * Socket receive timestamp support (SO_TIMESTAMP) + * ======================================================================== */ + +/** + * Enable SO_TIMESTAMP socket option for kernel receive timestamping + * + * Returns: + * 1 = SO_TIMESTAMPNS enabled (nanosecond precision) + * 0 = SO_TIMESTAMP enabled (microsecond precision) + * -1 = timestamps not available on this platform + */ +int +be_socket_enable_timestamps_(evutil_socket_t fd) +{ + int on = 1; + +#if EVENT__HAVE_DECL_SO_TIMESTAMPNS + /* Try nanosecond precision first (Linux 2.6.22+) */ + if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPNS, &on, sizeof(on)) == 0) { + return 1; + } +#endif + +#if EVENT__HAVE_DECL_SO_TIMESTAMP + /* Fall back to microsecond precision */ + if (setsockopt(fd, SOL_SOCKET, SO_TIMESTAMP, &on, sizeof(on)) == 0) { + return 0; + } +#endif + + return -1; +} + const struct bufferevent_ops bufferevent_ops_socket = { "socket", evutil_offsetof(struct bufferevent_private, bev), @@ -191,7 +225,15 @@ bufferevent_readcb(evutil_socket_t fd, short event, void *arg) goto done; evbuffer_unfreeze(input, 0); - res = evbuffer_read(input, fd, (int)howmuch); /* XXXX evbuffer_read would do better to take and return ev_ssize_t */ + + if (bufev_p->recv_timestamps_enabled) { + /* Use recvmsg() to capture timestamps */ + res = evbuffer_read_with_timestamp(input, fd, (int)howmuch); + } else { + /* Use standard read when timestamps not enabled */ + res = evbuffer_read(input, fd, (int)howmuch); + } + evbuffer_freeze(input, 0); if (res == -1) { @@ -374,6 +416,13 @@ bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, evbuffer_add_cb(bufev->output, bufferevent_socket_outbuf_cb, bufev); + /* Enable receive timestamps if requested */ + if ((options & BEV_OPT_RECV_TIMESTAMPS) && fd >= 0) { + if (be_socket_enable_timestamps_(fd) >= 0) { + bufev_p->recv_timestamps_enabled = 1; + } + } + evbuffer_freeze(bufev->input, 0); evbuffer_freeze(bufev->output, 1); @@ -641,9 +690,17 @@ be_socket_setfd(struct bufferevent *bufev, evutil_socket_t fd) event_assign(&bufev->ev_write, bufev->ev_base, fd, EV_WRITE|EV_PERSIST|EV_FINALIZE, bufferevent_writecb, bufev); - if (fd >= 0) + if (fd >= 0) { bufferevent_enable(bufev, bufev->enabled); + /* Enable receive timestamps if requested */ + if ((bufev_p->options & BEV_OPT_RECV_TIMESTAMPS) && !bufev_p->recv_timestamps_enabled) { + if (be_socket_enable_timestamps_(fd) >= 0) { + bufev_p->recv_timestamps_enabled = 1; + } + } + } + evutil_getaddrinfo_cancel_async_(bufev_p->dns_request); BEV_UNLOCK(bufev); diff --git a/configure.ac b/configure.ac index 38b6f247d..e5d115335 100644 --- a/configure.ac +++ b/configure.ac @@ -342,6 +342,13 @@ if test "x$ac_cv_header_sys_sysctl_h" = "xyes"; then ) fi +if test "x$ac_cv_header_sys_socket_h" = "xyes"; then + AC_CHECK_DECLS([SO_TIMESTAMP, SO_TIMESTAMPNS], [], [], + [[#include + #include ]] + ) +fi + AM_CONDITIONAL(BUILD_WIN32, test x$bwin32 = xtrue) AM_CONDITIONAL(BUILD_CYGWIN, test x$cygwin = xtrue) AM_CONDITIONAL(BUILD_MIDIPIX, test x$midipix = xtrue) diff --git a/evbuffer-internal.h b/evbuffer-internal.h index d09b4f1dd..d24bad202 100644 --- a/evbuffer-internal.h +++ b/evbuffer-internal.h @@ -204,6 +204,15 @@ struct evbuffer_chain { /** number of references to this chain */ int refcnt; + + /** Timestamp support. */ + struct { + /* The timespec for the oldest data in this chunk */ + struct timespec ts; + /* valid is set to a non-zero value when ts is set */ + int valid; + } timestamp; + /** Usually points to the read-write memory belonging to this * buffer allocated as part of the evbuffer_chain allocation. * For mmap, this can be a read-only buffer and diff --git a/event-config.h.cmake b/event-config.h.cmake index fccf0cf05..81691c40a 100644 --- a/event-config.h.cmake +++ b/event-config.h.cmake @@ -78,6 +78,15 @@ /* Define to 1 if you have `getrandom' function. */ #cmakedefine EVENT__HAVE_GETRANDOM 1 +/* Define to 1 if you have the declaration of `SO_TIMESTAMP'. */ +#define EVENT__HAVE_DECL_SO_TIMESTAMP @EVENT__HAVE_DECL_SO_TIMESTAMP@ + +/* Define to 1 if you have the declaration of `SO_TIMESTAMPNS'. */ +#define EVENT__HAVE_DECL_SO_TIMESTAMPNS @EVENT__HAVE_DECL_SO_TIMESTAMPNS@ + +/* Define to 1 if you have `getrandom' function. */ +#cmakedefine EVENT__HAVE_GETRANDOM 1 + /* Define if /dev/poll is available */ #cmakedefine EVENT__HAVE_DEVPOLL 1 diff --git a/include/event2/buffer.h b/include/event2/buffer.h index 88af3ae14..b5513ce20 100644 --- a/include/event2/buffer.h +++ b/include/event2/buffer.h @@ -324,6 +324,23 @@ EVENT2_EXPORT_SYMBOL int evbuffer_commit_space(struct evbuffer *buf, struct evbuffer_iovec *vec, int n_vecs); +/** + Commits the space reserved by evbuffer_reserve_space() and + associates a timespec with the committed chains. + + @param buf the evbuffer in which to reserve space. + @param vec one or two extents returned by evbuffer_reserve_space. + @param n_vecs the number of extents. + @param ts pointer to timespec (or NULL if not valid). + @return 0 on success, -1 on error + @see evbuffer_reserve_space() +*/ +EVENT2_EXPORT_SYMBOL +int evbuffer_commit_space_with_timespec(struct evbuffer *buf, + struct evbuffer_iovec *vec, int n_vecs, + const struct timespec *ts); + + /** Append data to the end of an evbuffer. @@ -734,6 +751,43 @@ int evbuffer_write_atmost(struct evbuffer *buffer, evutil_socket_t fd, EVENT2_EXPORT_SYMBOL int evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch); +/** + Read from a file descriptor and store the result in an evbuffer. + + @param buffer the evbuffer to store the result + @param fd the file descriptor to read from + @param howmuch the number of bytes to be read (if howmuch < 0 or + higher than EVBUFFER_MAX_READ it is set to + EVBUFFER_MAX_READ) + @return the number of bytes read, or -1 if an error occurred + @see evbuffer_write() + */ +EVENT2_EXPORT_SYMBOL +int evbuffer_read_with_timestamp(struct evbuffer *buffer, evutil_socket_t fd, + int howmuch); + +/** + * Get the timestamp stored for the oldest data in the buffer chain. + * + * Returns the timestamp of when the oldest bytes currently in the buffer + * were received from the kernel. This is the timestamp of the first chain + * in the buffer. If the buffer is empty or no timestamp is available, + * returns -1. + * + * Note: Timestamps are stored per internal chain. When evbuffer_pullup() + * consolidates multiple chains, only the timestamp from the first (oldest) + * chain is preserved. Also, reads may append new data into an existing chain + * that already has a timestamp; in that case, draining some (but not all) + * bytes from that chain will not change the reported timestamp. + * + * @param buffer The buffer to read from + * @param timestamp where to store the result + * @return 0 success (timestamp was stored) + * -1 failure (buffer empty or no timestamp available) + */ +EVENT2_EXPORT_SYMBOL +int evbuffer_get_timestamp(struct evbuffer *buffer, struct timespec *timestamp); + /** Search for a string within an evbuffer. diff --git a/include/event2/bufferevent.h b/include/event2/bufferevent.h index 48cd15356..8bd601e4e 100644 --- a/include/event2/bufferevent.h +++ b/include/event2/bufferevent.h @@ -170,7 +170,19 @@ enum bufferevent_options { * bufferevent. This option currently requires that * BEV_OPT_DEFER_CALLBACKS also be set; a future version of Libevent * might remove the requirement.*/ - BEV_OPT_UNLOCK_CALLBACKS = (1<<3) + BEV_OPT_UNLOCK_CALLBACKS = (1<<3), + + /** If set, capture kernel-measured receive timestamps for socket + * bufferevents. Timestamps can be retrieved from the input buffer + * using evbuffer_get_timestamp(). Supported for socket bufferevents + * created with bufferevent_socket_new() and OpenSSL socket bufferevents + * created with bufferevent_openssl_socket_new(). + * + * Note: enabling this option bypasses rate limiting on reads. On Linux, + * the full message is fetched from the kernel (via MSG_PEEK|MSG_TRUNC) + * to determine the exact datagram size before reading, which may exceed + * any configured read limit. */ + BEV_OPT_RECV_TIMESTAMPS = (1<<4) }; /** diff --git a/test/regress_buffer.c b/test/regress_buffer.c index d4e1190af..3a19e1704 100644 --- a/test/regress_buffer.c +++ b/test/regress_buffer.c @@ -453,10 +453,147 @@ test_evbuffer_pullup_with_empty(void *ptr) tt_mem_op(evbuffer_pullup(buf, 3), ==, "foo", 3); end: - if (buf) + if (buf) { evbuffer_free(buf); + } } +static void +test_evbuffer_get_timestamp(void *ptr) +{ + struct evbuffer *buf = NULL; + struct timespec ts, ts2; + struct timeval tv_sleep = { 0, 10000 }; /* 10 ms */ + int on = 1; + int r; + int ts_supported = 1; + + struct sockaddr_in sin; + ev_socklen_t slen = sizeof(sin); + evutil_socket_t listener = -1; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + /* 1. Ensure empty buffer returns -1 */ + buf = evbuffer_new(); + tt_assert(buf); + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, -1); + + /* Create UDP loopback connection */ + listener = socket(AF_INET, SOCK_DGRAM, 0); + tt_assert(listener != EVUTIL_INVALID_SOCKET); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(0x7f000001L); + sin.sin_port = 0; + tt_assert(bind(listener, (struct sockaddr *)&sin, sizeof(sin)) == 0); + tt_assert(getsockname(listener, (struct sockaddr *)&sin, &slen) == 0); + + fd_pair[0] = socket(AF_INET, SOCK_DGRAM, 0); + tt_assert(fd_pair[0] != EVUTIL_INVALID_SOCKET); + tt_assert(connect(fd_pair[0], (struct sockaddr *)&sin, sizeof(sin)) == 0); + + fd_pair[1] = listener; + listener = -1; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(0x7f000001L); + tt_assert(getsockname(fd_pair[0], (struct sockaddr *)&sin, &slen) == 0); + tt_assert(connect(fd_pair[1], (struct sockaddr *)&sin, sizeof(sin)) == 0); + + evutil_make_socket_nonblocking(fd_pair[0]); + evutil_make_socket_nonblocking(fd_pair[1]); + + /* 2. Configure socket option for receive timestamps */ +#ifdef SO_TIMESTAMPNS + if (setsockopt(fd_pair[1], SOL_SOCKET, SO_TIMESTAMPNS, &on, sizeof(on)) == -1) { + ts_supported = 0; + } +#elif defined(SO_TIMESTAMP) + if (setsockopt(fd_pair[1], SOL_SOCKET, SO_TIMESTAMP, &on, sizeof(on)) == -1) { + ts_supported = 0; + } +#else + ts_supported = 0; +#endif + + /* If timestamps not supported, skip the timestamp checks */ + if (!ts_supported) { + tt_skip(); + goto end; + } + + /* 3. Send packet A */ + r = send(fd_pair[0], "packetA", 7, 0); + tt_int_op(r, ==, 7); + + /* Sleep briefly to let the kernel process the packet and stamp it */ + evutil_usleep_(&tv_sleep); + + /* 4. Read packet A with timestamp */ + r = evbuffer_read_with_timestamp(buf, fd_pair[1], 1024); + tt_int_op(r, ==, 7); + + /* 5. Fetch and verify timestamp A */ + tt_int_op(evbuffer_get_timestamp(buf, &ts), ==, 0); + tt_assert(ts.tv_sec > 0); + TT_BLATHER(("Captured timestamp A: %lld.%09ld", (long long)ts.tv_sec, (long)ts.tv_nsec)); + + /* 6. Send packet B */ + r = send(fd_pair[0], "packetB", 7, 0); + tt_int_op(r, ==, 7); + + evutil_usleep_(&tv_sleep); + + /* 7. Read packet B with timestamp */ + r = evbuffer_read_with_timestamp(buf, fd_pair[1], 1024); + tt_int_op(r, ==, 7); + + /* 8. Fetch timestamp and verify it still returns packet A's (oldest first) */ + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, 0); + tt_int_op(ts.tv_sec, ==, ts2.tv_sec); + tt_int_op(ts.tv_nsec, ==, ts2.tv_nsec); + + /* 9. Drain packet A's bytes. Packet A is 7 bytes. + * Draining 3 bytes should still keep packet A's timestamp. */ + tt_int_op(evbuffer_drain(buf, 3), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, 0); + tt_int_op(ts.tv_sec, ==, ts2.tv_sec); + tt_int_op(ts.tv_nsec, ==, ts2.tv_nsec); + + /* 10. Drain remaining 4 bytes of packet A. + * Each recvmsg() call writes into its own fresh chain, so after fully + * draining packet A the buffer exposes packet B's chain and its + * timestamp. The timestamp must be >= packet A's timestamp. */ + tt_int_op(evbuffer_drain(buf, 4), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, 0); + tt_assert(ts2.tv_sec >= ts.tv_sec); + if (ts2.tv_sec == ts.tv_sec) { + tt_assert(ts2.tv_nsec >= ts.tv_nsec); + } + TT_BLATHER(("Captured oldest timestamp after draining A: %lld.%09ld", + (long long)ts2.tv_sec, (long)ts2.tv_nsec)); + + /* 11. Fully drain the buffer. Assert evbuffer_get_timestamp returns -1. */ + tt_int_op(evbuffer_drain(buf, 7), ==, 0); + tt_int_op(evbuffer_get_timestamp(buf, &ts2), ==, -1); + + end: + if (buf) { + evbuffer_free(buf); + } + if (listener != -1) { + evutil_closesocket(listener); + } + if (fd_pair[0] != -1) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] != -1) { + evutil_closesocket(fd_pair[1]); + } +} + + static void test_evbuffer_remove_buffer_with_empty_front(void *ptr) { @@ -2878,6 +3015,7 @@ struct testcase_t evbuffer_testcases[] = { { "copyout", test_evbuffer_copyout, 0, NULL, NULL}, { "file_segment_add_cleanup_cb", test_evbuffer_file_segment_add_cleanup_cb, 0, NULL, NULL }, { "pullup_with_empty", test_evbuffer_pullup_with_empty, 0, NULL, NULL }, + { "get_timestamp", test_evbuffer_get_timestamp, TT_FORK, &basic_setup, NULL }, #define ADDFILE_TEST(name, parameters) \ { name, test_evbuffer_add_file, TT_FORK|TT_NEED_BASE, \ diff --git a/test/regress_bufferevent.c b/test/regress_bufferevent.c index c276a0e5d..970acfd76 100644 --- a/test/regress_bufferevent.c +++ b/test/regress_bufferevent.c @@ -1354,6 +1354,120 @@ test_bufferevent_filter_data_stuck(void *arg) bufferevent_free(filter); } +static void +bufferevent_recv_timestamps_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + char tmp[32]; + int r; + + /* Fetch and verify timestamps BEFORE draining the buffer! */ + if (evbuffer_get_timestamp(bufferevent_get_input(bev), &ts) < 0) { + /* Timestamp capture not supported/enabled on this platform/socket. */ + *done = 0; + event_base_loopexit(bufferevent_get_base(bev), NULL); + goto end; + } + + tt_assert(ts.tv_sec > 0); + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 14); + tt_mem_op(tmp, ==, "timestamp_test", 14); + + *done = 1; + event_base_loopexit(bufferevent_get_base(bev), NULL); + + end: + ; +} + +static void +test_bufferevent_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + struct timespec ts; + int done = 0; + + struct sockaddr_in sin; + ev_socklen_t slen = sizeof(sin); + evutil_socket_t listener = -1; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + /* Create UDP loopback connection */ + listener = socket(AF_INET, SOCK_DGRAM, 0); + tt_assert(listener != EVUTIL_INVALID_SOCKET); + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(0x7f000001L); + sin.sin_port = 0; + tt_assert(bind(listener, (struct sockaddr *)&sin, sizeof(sin)) == 0); + tt_assert(getsockname(listener, (struct sockaddr *)&sin, &slen) == 0); + + fd_pair[0] = socket(AF_INET, SOCK_DGRAM, 0); + tt_assert(fd_pair[0] != EVUTIL_INVALID_SOCKET); + tt_assert(connect(fd_pair[0], (struct sockaddr *)&sin, sizeof(sin)) == 0); + + fd_pair[1] = listener; + listener = -1; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = htonl(0x7f000001L); + tt_assert(getsockname(fd_pair[0], (struct sockaddr *)&sin, &slen) == 0); + tt_assert(connect(fd_pair[1], (struct sockaddr *)&sin, sizeof(sin)) == 0); + + /* 1. Create bufferevents (bev2 has BEV_OPT_RECV_TIMESTAMPS enabled) */ + bev1 = bufferevent_socket_new(data->base, fd_pair[0], BEV_OPT_CLOSE_ON_FREE); + tt_assert(bev1); + fd_pair[0] = -1; /* bev1 owns it now */ + bev2 = bufferevent_socket_new(data->base, fd_pair[1], BEV_OPT_CLOSE_ON_FREE | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; /* bev2 owns it now */ + + /* 2. Verify that initially no timestamps are present */ + tt_int_op(evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts), ==, -1); + + /* 3. Enable writing on bev1 and write data */ + tt_int_op(bufferevent_enable(bev1, EV_WRITE), ==, 0); + tt_int_op(bufferevent_write(bev1, "timestamp_test", 14), ==, 0); + + /* Configure callback and enable read on bev2 */ + bufferevent_setcb(bev2, bufferevent_recv_timestamps_readcb, NULL, NULL, &done); + tt_int_op(bufferevent_enable(bev2, EV_READ), ==, 0); + + /* 4. Dispatch event loop and wait for arrival */ + event_base_dispatch(data->base); + + /* If timestamp capture failed (not supported), skip the test */ + if (done == 0) { + tt_skip(); + goto end; + } + + tt_int_op(done, ==, 1); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (listener != -1) { + evutil_closesocket(listener); + } + if (fd_pair[0] != -1) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] != -1) { + evutil_closesocket(fd_pair[1]); + } +} + + struct testcase_t bufferevent_testcases[] = { LEGACY(bufferevent, TT_ISOLATED), @@ -1429,6 +1543,9 @@ struct testcase_t bufferevent_testcases[] = { { "bufferevent_filter_data_stuck", test_bufferevent_filter_data_stuck, TT_FORK|TT_NEED_BASE, &basic_setup, NULL }, + { "bufferevent_recv_timestamps", + test_bufferevent_recv_timestamps, + TT_FORK|TT_NEED_BASE, &basic_setup, NULL }, END_OF_TESTCASES, }; diff --git a/test/regress_ssl.c b/test/regress_ssl.c index e7c4023e0..b5eb5181b 100644 --- a/test/regress_ssl.c +++ b/test/regress_ssl.c @@ -988,6 +988,244 @@ regress_bufferevent_openssl_wm(void *arg) event_base_loop(base, EVLOOP_ONCE); } +static void +bufferevent_openssl_recv_timestamps_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + struct evbuffer *input; + char tmp[32]; + int r; + int ts_result; + + /* Fetch and verify timestamps BEFORE draining the buffer! */ + input = bufferevent_get_input(bev); + ts_result = evbuffer_get_timestamp(input, &ts); + + /* UNIX domain sockets don't support kernel timestamps on most + * platforms, so timestamp may not be available. Just verify we can + * read the data successfully. If kernel timestamp is available, the + * caller would have set ts_result == 0. */ + if (ts_result == 0) { + tt_assert(ts.tv_sec > 0); + } + + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 14); + tt_mem_op(tmp, ==, "timestamp_test", 14); + + *done = 1; + event_base_loopexit(bufferevent_get_base(bev), NULL); + + end: + ; +} + +static void +test_eventcb(struct bufferevent *bev, short what, void *ctx) +{ + TT_BLATHER(("test_eventcb: %p got event %d", bev, (int)what)); + if (what & BEV_EVENT_ERROR) { + unsigned long err; + while ((err = ERR_get_error())) { + TT_BLATHER((" SSL error: %s", ERR_error_string(err, NULL))); + } + } +} + +static void +test_bufferevent_openssl_direct_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + SSL *ssl1 = NULL, *ssl2 = NULL; + struct timespec ts; + int done = 0; + evutil_socket_t fd_pair[2] = { -1, -1 }; + + /* Create a socketpair */ + #ifdef _WIN32 + tt_assert(evutil_socketpair(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + #else + tt_assert(evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fd_pair) == 0); + #endif + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + ssl1 = SSL_new(get_ssl_ctx()); + ssl2 = SSL_new(get_ssl_ctx()); + tt_assert(ssl1); + tt_assert(ssl2); + + SSL_use_certificate(ssl2, the_cert); + SSL_use_PrivateKey(ssl2, the_key); + + /* Create direct socket openssl bufferevents. + * bev2 has BEV_OPT_RECV_TIMESTAMPS enabled. */ + bev1 = bufferevent_openssl_socket_new( + data->base, fd_pair[0], ssl1, BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(bev1); + fd_pair[0] = -1; + + bev2 = bufferevent_openssl_socket_new( + data->base, fd_pair[1], ssl2, BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(bev2); + fd_pair[1] = -1; + + /* Verify initially no timestamps are present */ + tt_int_op(evbuffer_get_timestamp(bufferevent_get_input(bev2), &ts), ==, -1); + + /* Configure callbacks */ + bufferevent_setcb(bev1, NULL, NULL, test_eventcb, NULL); + bufferevent_setcb(bev2, bufferevent_openssl_recv_timestamps_readcb, NULL, test_eventcb, &done); + tt_int_op(bufferevent_enable(bev1, EV_READ|EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ|EV_WRITE), ==, 0); + + /* Write data from bev1 */ + tt_int_op(bufferevent_write(bev1, "timestamp_test", 14), ==, 0); + + /* Dispatch base */ + event_base_dispatch(data->base); + + tt_int_op(done, ==, 1); + + end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + +static void +bufferevent_openssl_filter_recv_timestamps_readcb(struct bufferevent *bev, void *ctx) +{ + int *done = ctx; + struct timespec ts; + struct evbuffer *input; + char tmp[32]; + int r; + int ts_result; + + input = bufferevent_get_input(bev); + ts_result = evbuffer_get_timestamp(input, &ts); + + /* UNIX domain sockets don't support kernel timestamps on most + * platforms, so timestamp may not be available. Just verify we can + * read the data successfully. If kernel timestamp is available, the + * caller would have set ts_result == 0. */ + if (ts_result == 0) { + tt_assert(ts.tv_sec > 0); + } + + r = bufferevent_read(bev, tmp, sizeof(tmp)); + tt_int_op(r, ==, 9); + tt_mem_op(tmp, ==, "test_data", 9); + + *done = 1; + event_base_loopexit(bufferevent_get_base(bev), NULL); + + end: + ; +} + +static void +test_bufferevent_openssl_filter_recv_timestamps(void *arg) +{ + struct basic_test_data *data = arg; + struct bufferevent *bev1 = NULL; + struct bufferevent *bev2 = NULL; + struct bufferevent *underlying_bev1 = NULL; + struct bufferevent *underlying_bev2 = NULL; + SSL *ssl1 = NULL, *ssl2 = NULL; + int done = 0; + evutil_socket_t fd_pair[2] = {-1, -1}; + + #ifdef _WIN32 + tt_assert(evutil_socketpair(AF_INET, SOCK_STREAM, 0, fd_pair) == 0); + #else + /* Create UNIX domain socketpair */ + tt_assert(evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fd_pair) == 0); + #endif + tt_assert(evutil_make_socket_nonblocking(fd_pair[0]) == 0); + tt_assert(evutil_make_socket_nonblocking(fd_pair[1]) == 0); + + ssl1 = SSL_new(get_ssl_ctx()); + ssl2 = SSL_new(get_ssl_ctx()); + tt_assert(ssl1); + tt_assert(ssl2); + + SSL_use_certificate(ssl2, the_cert); + SSL_use_PrivateKey(ssl2, the_key); + + /* Create underlying socket bufferevents */ + underlying_bev1 = bufferevent_socket_new(data->base, fd_pair[0], + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(underlying_bev1); + fd_pair[0] = -1; + + underlying_bev2 = bufferevent_socket_new(data->base, fd_pair[1], + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS | BEV_OPT_RECV_TIMESTAMPS); + tt_assert(underlying_bev2); + fd_pair[1] = -1; + + /* Create filtered openssl bufferevents that wrap the socket bufferevents */ + bev1 = bufferevent_openssl_filter_new(data->base, underlying_bev1, ssl1, + BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(bev1); + underlying_bev1 = NULL; /* ownership transferred */ + + bev2 = bufferevent_openssl_filter_new(data->base, underlying_bev2, ssl2, + BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + tt_assert(bev2); + underlying_bev2 = NULL; /* ownership transferred */ + + /* Configure callbacks for basic data flow */ + bufferevent_setcb(bev1, NULL, NULL, test_eventcb, NULL); + bufferevent_setcb(bev2, bufferevent_openssl_filter_recv_timestamps_readcb, NULL, test_eventcb, &done); + tt_int_op(bufferevent_enable(bev1, EV_READ | EV_WRITE), ==, 0); + tt_int_op(bufferevent_enable(bev2, EV_READ | EV_WRITE), ==, 0); + + /* Write data from bev1 */ + tt_int_op(bufferevent_write(bev1, "test_data", 9), ==, 0); + + /* Dispatch base - just ensure filtered mode with timestamp code paths works + */ + event_base_dispatch(data->base); + +end: + if (bev1) { + bufferevent_free(bev1); + } + if (bev2) { + bufferevent_free(bev2); + } + if (underlying_bev1) { + bufferevent_free(underlying_bev1); + } + if (underlying_bev2) { + bufferevent_free(underlying_bev2); + } + if (fd_pair[0] >= 0) { + evutil_closesocket(fd_pair[0]); + } + if (fd_pair[1] >= 0) { + evutil_closesocket(fd_pair[1]); + } +} + struct testcase_t ssl_testcases[] = { #define T(a) ((void *)(a)) { "bufferevent_socketpair", regress_bufferevent_openssl, @@ -1071,6 +1309,10 @@ struct testcase_t ssl_testcases[] = { TT_FORK|TT_NEED_BASE, &ssl_setup, T(REGRESS_DEFERRED_CALLBACKS) }, { "bufferevent_wm_filter_defer", regress_bufferevent_openssl_wm, TT_FORK|TT_NEED_BASE, &ssl_setup, T(REGRESS_OPENSSL_FILTER|REGRESS_DEFERRED_CALLBACKS) }, + { "bufferevent_openssl_direct_recv_timestamps", test_bufferevent_openssl_direct_recv_timestamps, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, + { "bufferevent_openssl_filter_recv_timestamps", test_bufferevent_openssl_filter_recv_timestamps, + TT_FORK|TT_NEED_BASE, &ssl_setup, NULL }, #undef T From 64c25ae4f06c8867b3b9ef948bf5fafd549395db Mon Sep 17 00:00:00 2001 From: Trond Norbye Date: Thu, 16 Jul 2026 16:36:40 +0200 Subject: [PATCH 3/4] Fix potential use after free --- buffer.c | 38 +++++++++++++++++++++++++++++--------- test/regress_buffer.c | 7 +++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/buffer.c b/buffer.c index 2d054f25e..ae7f6b731 100644 --- a/buffer.c +++ b/buffer.c @@ -2363,6 +2363,7 @@ evbuffer_read_impl_(struct evbuffer *buf, evutil_socket_t fd, int howmuch, int u struct evbuffer_chain **chainp; int n; int result; + struct evbuffer_chain *new_chain = NULL; #ifdef USE_IOVEC_IMPL int nvecs, i, remaining; @@ -2399,14 +2400,18 @@ evbuffer_read_impl_(struct evbuffer *buf, evutil_socket_t fd, int howmuch, int u if (use_recvmsg) { /* Allocate a fresh chain for each recvmsg() call so that * every call gets its own chain with an independent timestamp. */ - struct evbuffer_chain *old_last = buf->last; - struct evbuffer_chain *new_chain = evbuffer_chain_new(howmuch); + struct evbuffer_chain **tp; + new_chain = evbuffer_chain_new(howmuch); if (!new_chain) { result = -1; goto done; } evbuffer_chain_insert(buf, new_chain); - chainp = old_last ? &old_last->next : &buf->first; + tp = buf->last_with_datap; + while (*tp && *tp != new_chain) { + tp = &(*tp)->next; + } + chainp = tp; nvecs = 1; vecs[0].iov_base = (void *)CHAIN_SPACE_PTR(new_chain); vecs[0].iov_len = (size_t)howmuch; @@ -2532,12 +2537,27 @@ evbuffer_read_impl_(struct evbuffer *buf, evutil_socket_t fd, int howmuch, int u #endif #endif /* USE_IOVEC_IMPL */ - if (n == -1) { - result = -1; - goto done; - } - if (n == 0) { - result = 0; + if (n <= 0) { + result = n; +#ifndef _WIN32 + if (new_chain) { + struct evbuffer_chain *prev = NULL; + if (buf->first != new_chain) { + prev = buf->first; + while (prev->next != new_chain) { + prev = prev->next; + } + } + if (prev) { + prev->next = NULL; + buf->last = prev; + } else { + buf->first = NULL; + buf->last = NULL; + } + evbuffer_chain_free(new_chain); + } +#endif goto done; } diff --git a/test/regress_buffer.c b/test/regress_buffer.c index 3a19e1704..5a7bead43 100644 --- a/test/regress_buffer.c +++ b/test/regress_buffer.c @@ -523,6 +523,13 @@ test_evbuffer_get_timestamp(void *ptr) goto end; } + /* Test EAGAIN handling and ensure no trailing empty chain remains */ + r = evbuffer_read_with_timestamp(buf, fd_pair[1], 1024); + tt_int_op(r, ==, -1); + tt_assert(errno == EAGAIN || errno == EWOULDBLOCK); + tt_ptr_op(buf->first, ==, NULL); + tt_ptr_op(buf->last, ==, NULL); + /* 3. Send packet A */ r = send(fd_pair[0], "packetA", 7, 0); tt_int_op(r, ==, 7); From f7d2fcffbd246c5240b06f5a710f3eabf3049442 Mon Sep 17 00:00:00 2001 From: Trond Norbye Date: Thu, 16 Jul 2026 16:58:26 +0200 Subject: [PATCH 4/4] Remove the incorrect size check for receiving data Note that this would cause truncation of data over UDP (but that is no different from the "old" code as it also suffers from that) --- buffer.c | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/buffer.c b/buffer.c index ae7f6b731..fe3a48502 100644 --- a/buffer.c +++ b/buffer.c @@ -2305,18 +2305,8 @@ evbuffer_read_setup_vecs_(struct evbuffer *buf, ev_ssize_t howmuch, } static int -get_n_bytes_readable_on_socket(evutil_socket_t fd, int use_recvmsg) +get_n_bytes_readable_on_socket(evutil_socket_t fd) { -#if !defined(_WIN32) && !defined(__APPLE__) - if (use_recvmsg) { - int r = (int)recv(fd, NULL, 0, MSG_PEEK | MSG_TRUNC); - if (r > 0) { - return r; - } - return EVBUFFER_MAX_READ; - } -#endif - #if defined(FIONREAD) && defined(_WIN32) unsigned long lng = EVBUFFER_MAX_READ; if (ioctlsocket(fd, FIONREAD, &lng) < 0) @@ -2324,12 +2314,10 @@ get_n_bytes_readable_on_socket(evutil_socket_t fd, int use_recvmsg) /* Can overflow, but mostly harmlessly. XXXX */ return (int)lng; #elif defined(FIONREAD) - { - int n = EVBUFFER_MAX_READ; - if (ioctl(fd, FIONREAD, &n) < 0) - return -1; - return n; - } + int n = EVBUFFER_MAX_READ; + if (ioctl(fd, FIONREAD, &n) < 0) + return -1; + return n; #else return EVBUFFER_MAX_READ; #endif @@ -2382,14 +2370,9 @@ evbuffer_read_impl_(struct evbuffer *buf, evutil_socket_t fd, int howmuch, int u goto done; } - n = get_n_bytes_readable_on_socket(fd, use_recvmsg); - if (n <= 0 || (n > EVBUFFER_MAX_READ && !use_recvmsg)) + n = get_n_bytes_readable_on_socket(fd); + if (n <= 0 || n > EVBUFFER_MAX_READ) n = EVBUFFER_MAX_READ; -#if !defined(_WIN32) && !defined(__APPLE__) - if (use_recvmsg && n > howmuch) { - howmuch = n; - } -#endif if (howmuch < 0 || howmuch > n) howmuch = n;