From 59c663000872ea1a4578e0504e1139bd0f434ad5 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 22 Jul 2026 18:20:13 +0200 Subject: [PATCH 1/3] erts: Reuse a per-thread buffer pool in essio_recvmmsg essio_recvmmsg allocated (zeroed, and freed) the entire vlen*(bufSz+ctrlSz) + metadata scratch block, plus a second block for the result-term array, on every call. At vlen=64, bufSz=2048, ctrlSz=1024 that is ~197 KB of malloc+memzero+free to receive even one datagram. Keep one grow-to-fit block per scheduler thread via thread-specific data. A recvmmsg NIF call runs start-to-finish on a single scheduler thread and uses the block only within that call (received data is copied out into fresh binaries before returning), so no locking is needed and no two calls ever touch the same block concurrently. Retained memory is bounded by the number of scheduler threads that have run recvmmsg (normal + dirty-IO), independent of the number of sockets. The result-term array is carved out of the same block. Add recvmmsg_pool_reuse_udp4 to socket_SUITE, exercising pool reuse and growth across many calls with varying VLen/BufSz. --- erts/emulator/nifs/unix/unix_socket_syncio.c | 84 ++++++++++++++------ lib/kernel/test/socket_SUITE.erl | 50 +++++++++++- 2 files changed, 109 insertions(+), 25 deletions(-) diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index 3049d287f775..deb8b4d100e4 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -335,6 +335,37 @@ typedef struct { } ESSIOControl; +#ifdef HAVE_RECVMMSG +/* Grow-to-fit scratch block for essio_recvmmsg, kept per scheduler thread. */ +typedef struct { + char* base; + size_t capacity; +} ESSIOMMsgPool; + +static ErlNifTSDKey esock_mmsg_pool_key; + +/* Return this thread's scratch block, grown (grow-only) to hold 'need' bytes. */ +static ESSIOMMsgPool* essio_recvmmsg_pool(const size_t need) +{ + ESSIOMMsgPool* pool = enif_tsd_get(esock_mmsg_pool_key); + if (pool == NULL) { + ESOCK_ASSERT( (pool = MALLOC(sizeof(ESSIOMMsgPool))) != NULL ); + pool->base = NULL; + pool->capacity = 0; + enif_tsd_set(esock_mmsg_pool_key, pool); + } + if (pool->capacity < need) { + char* nbase = (pool->base == NULL) ? + (char*) MALLOC(need) : (char*) REALLOC(pool->base, need); + ESOCK_ASSERT( nbase != NULL ); + pool->base = nbase; + pool->capacity = need; + } + return pool; +} +#endif /* HAVE_RECVMMSG */ + + /* ======================================================================== * * Function Forwards * * ======================================================================== * @@ -1112,6 +1143,11 @@ int essio_init(unsigned int numThreads, essio_sctp_init(); +#ifdef HAVE_RECVMMSG + ESOCK_ASSERT( enif_tsd_key_create("esock_mmsg_pool", + &esock_mmsg_pool_key) == 0 ); +#endif + return ESOCK_IO_OK; } @@ -4125,7 +4161,8 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, struct iovec* recvIovecs = NULL; ErlNifBinary* bufs = NULL; ErlNifBinary* ctrls = NULL; - char* heapPool = NULL; + ERL_NIF_TERM* elems = NULL; + ESSIOMMsgPool* pool; SOCKLEN_T addrLen = sizeof(ESockAddress); size_t bufSz = (bufLen != 0 ? bufLen : descP->rBufSz); size_t ctrlSz = (ctrlLen != 0 ? ctrlLen : descP->rCtrlSz); @@ -4158,21 +4195,29 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, size_t addrs_sz = vlen * sizeof(ESockAddress); size_t mmsghdrs_sz = vlen * sizeof(struct mmsghdr); size_t iovecs_sz = vlen * sizeof(struct iovec); + size_t elems_sz = vlen * sizeof(ERL_NIF_TERM); + size_t meta_sz = bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz + iovecs_sz + elems_sz; size_t bufdata_sz = vlen * bufSz; size_t ctrldata_sz = vlen * ctrlSz; - size_t total_sz = bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz + iovecs_sz + bufdata_sz + ctrldata_sz; - ESOCK_ASSERT((heapPool = (char*) MALLOC(total_sz)) != NULL ); - sys_memzero(heapPool, bufs_sz + ctrls_sz + addrs_sz); - bufs = (ErlNifBinary*) (heapPool); - ctrls = (ErlNifBinary*) (heapPool + bufs_sz); - addrs = (ESockAddress*) (heapPool + bufs_sz + ctrls_sz); - recvMmsghdrs = (struct mmsghdr*) (heapPool + bufs_sz + ctrls_sz + addrs_sz); - recvIovecs = (struct iovec*) (heapPool + bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz); - recvBufs = (unsigned char*) (heapPool + bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz + iovecs_sz); - recvCtrl = (unsigned char*) (heapPool + bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz + iovecs_sz + bufdata_sz); + size_t total_sz = meta_sz + bufdata_sz + ctrldata_sz; + char* p; + + pool = essio_recvmmsg_pool(total_sz); + p = pool->base; + + /* bufs/ctrls/addrs must start zeroed for the cleanup path. */ + sys_memzero(p, bufs_sz + ctrls_sz + addrs_sz); + bufs = (ErlNifBinary*) (p); + ctrls = (ErlNifBinary*) (p + bufs_sz); + addrs = (ESockAddress*) (p + bufs_sz + ctrls_sz); + recvMmsghdrs = (struct mmsghdr*) (p + bufs_sz + ctrls_sz + addrs_sz); + recvIovecs = (struct iovec*) (p + bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz); + elems = (ERL_NIF_TERM*) (p + bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz + iovecs_sz); + recvBufs = (unsigned char*) (p + meta_sz); + recvCtrl = (unsigned char*) (p + meta_sz + bufdata_sz); } - /* Set up mmsghdr structures to point into raw memory blocks */ + /* Set up mmsghdr/iovec slots to point into the block. */ for (i = 0; i < vlen; i++) { recvIovecs[i].iov_base = recvBufs + (i * bufSz); recvIovecs[i].iov_len = bufSz; @@ -4208,8 +4253,6 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, */ { size_t totalBytes = 0; - ERL_NIF_TERM* elems; - ESOCK_ASSERT( (elems = MALLOC(readResult * sizeof(ERL_NIF_TERM))) != NULL ); for (i = 0; i < (unsigned int) readResult; i++) { ErlNifBinary bin; @@ -4248,7 +4291,6 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, } resultList = enif_make_list_from_array(env, elems, readResult); - enif_free(elems); /* Update packet and byte counters */ ESOCK_CNT_INC(env, descP, sockRef, esock_atom_read_pkg, &descP->readPkgCnt, readResult); @@ -4268,13 +4310,9 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, } cleanup: - /* Free ErlNifBinary structures only for received messages. - * Note: recv_create_bin may have transferred ownership (set data = NULL), - * in which case FREE_BIN is a no-op. We only free binaries we still own. - * When exiting early from the allocation loop, i is at the index where - * allocation failed, so we free indices 0 to i-1. On successful completion, - * i equals readResult, so we free indices 0 to readResult-1. - */ + /* Free the binaries we still own; recv_create_bin may have handed some off + * (data == NULL -> FREE_BIN is a no-op). The bufs/ctrls arrays were zeroed + * above so slots the allocation loop never reached are skipped. */ { unsigned int countToFree = (i < (unsigned int) readResult) ? i : (unsigned int) readResult; if (countToFree > 0) { @@ -4289,8 +4327,6 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, } } - if (heapPool) enif_free(heapPool); - return ret; } #else /* HAVE_RECVMMSG */ diff --git a/lib/kernel/test/socket_SUITE.erl b/lib/kernel/test/socket_SUITE.erl index 4fa05a6e583d..f0aef048023d 100644 --- a/lib/kernel/test/socket_SUITE.erl +++ b/lib/kernel/test/socket_SUITE.erl @@ -158,6 +158,7 @@ sendmmsg_invalid_msg_format/1, recvmmsg_dirty_scheduler_udp4/1, sendmmsg_dirty_scheduler_udp4/1, + recvmmsg_pool_reuse_udp4/1, %% Socket IOCTL simple ioctl_simple1/1, @@ -393,7 +394,8 @@ batch_cases() -> sendmmsg_with_addresses_udp4, sendmmsg_invalid_msg_format, recvmmsg_dirty_scheduler_udp4, - sendmmsg_dirty_scheduler_udp4 + sendmmsg_dirty_scheduler_udp4, + recvmmsg_pool_reuse_udp4 ]. ioctl_cases() -> @@ -15811,6 +15813,52 @@ sendmmsg_dirty_scheduler_udp4(_Config) when is_list(_Config) -> ). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Many recvmmsg calls on one socket, varying VLen/BufSz, to exercise the +%% recvmmsg scratch-pool reuse and grow paths. +%% +recvmmsg_pool_reuse_udp4(_Config) when is_list(_Config) -> + ?TT(?SECS(60)), + tc_try( + recvmmsg_pool_reuse_udp4, + fun() -> + has_support_ipv4(), + has_recvmmsg_support() + end, + fun() -> + {ok, S1} = socket:open(inet, dgram, udp), + {ok, S2} = socket:open(inet, dgram, udp), + {ok, Addr} = inet:getaddr("localhost", inet), + ok = socket:bind(S1, #{family => inet, addr => Addr, port => 0}), + {ok, #{port := LocalPort}} = socket:sockname(S1), + ok = socket:connect(S2, + #{family => inet, addr => Addr, + port => LocalPort}), + %% Varying VLen/BufSz (repeated) -> grow and pure-reuse paths. + Rounds = [{5, 64}, {50, 2048}, {3, 512}, {120, 256}, + {10, 4096}, {1, 8}, {80, 1024}], + lists:foreach( + fun({VLen, BufSz}) -> + recvmmsg_pool_reuse_round(S1, S2, VLen, BufSz) + end, + Rounds ++ Rounds), + ok = socket:close(S1), + ok = socket:close(S2), + ok + end + ). + +recvmmsg_pool_reuse_round(S1, S2, VLen, BufSz) -> + Expected = [list_to_binary(io_lib:format("r~p_m~p", [VLen, N])) + || N <- lists:seq(1, VLen)], + lists:foreach(fun(D) -> ok = socket:send(S2, D) end, Expected), + {ok, Received} = socket:recvmmsg(S1, VLen, BufSz, 0, [], infinity), + VLen = length(Received), + ReceivedData = [Data || Msg <- Received, [Data] <- [maps:get(iov, Msg)]], + true = lists:sort(ReceivedData) =:= lists:sort(Expected), + ok. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Helper function to check if recvmmsg is supported %% From 39c9abe310afa201efe1afa864a674e8ae15e323 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 22 Jul 2026 18:21:56 +0200 Subject: [PATCH 2/3] erts: Make essio_recvmmsg per-call setup O(messages), not O(vlen) Now that the scratch block persists with a stable base pointer, the "stable" mmsghdr/iovec fields (buffer pointers, iov, msg_name) only change when the block moves or the per-slot dimensions (vlen, bufSz, ctrlSz) change. Do the full O(vlen) setup loop only on such a (re)layout; on a matching reuse, restore just the fields the kernel overwrites (msg_namelen, msg_controllen, msg_flags, msg_len) and the one post-processing overwrites (msg_control), and only for the leading slots the previous call actually used. A grow of the block only happens when total_sz (hence the dimensions) increased, so the layout check already covers it and no separate signal is needed. Also drop the per-call sys_memzero of the bufs/ctrls arrays: by keeping the post-processing index at 0 until the allocation loop, the empty/error paths free nothing and the success path only touches freshly allocated slots, so the arrays never need pre-zeroing. Together this makes the per-call cost O(messages received) rather than O(vlen). --- erts/emulator/nifs/unix/unix_socket_syncio.c | 73 ++++++++++++++------ lib/kernel/test/socket_SUITE.erl | 21 ++++-- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index deb8b4d100e4..d863d570be29 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -338,8 +338,12 @@ typedef struct { #ifdef HAVE_RECVMMSG /* Grow-to-fit scratch block for essio_recvmmsg, kept per scheduler thread. */ typedef struct { - char* base; - size_t capacity; + char* base; + size_t capacity; + unsigned int laid_vlen; /* dimensions the block is currently set up for */ + size_t laid_bufSz; + size_t laid_ctrlSz; + unsigned int used; /* leading slots the previous call mutated */ } ESSIOMMsgPool; static ErlNifTSDKey esock_mmsg_pool_key; @@ -350,8 +354,12 @@ static ESSIOMMsgPool* essio_recvmmsg_pool(const size_t need) ESSIOMMsgPool* pool = enif_tsd_get(esock_mmsg_pool_key); if (pool == NULL) { ESOCK_ASSERT( (pool = MALLOC(sizeof(ESSIOMMsgPool))) != NULL ); - pool->base = NULL; - pool->capacity = 0; + pool->base = NULL; + pool->capacity = 0; + pool->laid_vlen = 0; + pool->laid_bufSz = 0; + pool->laid_ctrlSz = 0; + pool->used = 0; enif_tsd_set(esock_mmsg_pool_key, pool); } if (pool->capacity < need) { @@ -4205,8 +4213,6 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, pool = essio_recvmmsg_pool(total_sz); p = pool->base; - /* bufs/ctrls/addrs must start zeroed for the cleanup path. */ - sys_memzero(p, bufs_sz + ctrls_sz + addrs_sz); bufs = (ErlNifBinary*) (p); ctrls = (ErlNifBinary*) (p + bufs_sz); addrs = (ESockAddress*) (p + bufs_sz + ctrls_sz); @@ -4215,20 +4221,43 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, elems = (ERL_NIF_TERM*) (p + bufs_sz + ctrls_sz + addrs_sz + mmsghdrs_sz + iovecs_sz); recvBufs = (unsigned char*) (p + meta_sz); recvCtrl = (unsigned char*) (p + meta_sz + bufdata_sz); - } - /* Set up mmsghdr/iovec slots to point into the block. */ - for (i = 0; i < vlen; i++) { - recvIovecs[i].iov_base = recvBufs + (i * bufSz); - recvIovecs[i].iov_len = bufSz; - recvMmsghdrs[i].msg_hdr.msg_name = &addrs[i]; - recvMmsghdrs[i].msg_hdr.msg_namelen = addrLen; - recvMmsghdrs[i].msg_hdr.msg_iov = &recvIovecs[i]; - recvMmsghdrs[i].msg_hdr.msg_iovlen = 1; - recvMmsghdrs[i].msg_hdr.msg_control = recvCtrl + (i * ctrlSz); - recvMmsghdrs[i].msg_hdr.msg_controllen = ctrlSz; - recvMmsghdrs[i].msg_hdr.msg_flags = 0; - recvMmsghdrs[i].msg_len = 0; + /* Full setup on (re)layout; otherwise only restore the fields the + * kernel and post-processing mutate, for the previously-used slots. + * A grow only happens when total_sz (hence the dimensions) changed, + * so the layout check below already covers it. 's' keeps 'i' at 0 + * until the allocation loop below. */ + if (vlen != pool->laid_vlen || + bufSz != pool->laid_bufSz || + ctrlSz != pool->laid_ctrlSz) { + unsigned int s; + for (s = 0; s < vlen; s++) { + recvIovecs[s].iov_base = recvBufs + (s * bufSz); + recvIovecs[s].iov_len = bufSz; + recvMmsghdrs[s].msg_hdr.msg_name = &addrs[s]; + recvMmsghdrs[s].msg_hdr.msg_namelen = addrLen; + recvMmsghdrs[s].msg_hdr.msg_iov = &recvIovecs[s]; + recvMmsghdrs[s].msg_hdr.msg_iovlen = 1; + recvMmsghdrs[s].msg_hdr.msg_control = recvCtrl + (s * ctrlSz); + recvMmsghdrs[s].msg_hdr.msg_controllen = ctrlSz; + recvMmsghdrs[s].msg_hdr.msg_flags = 0; + recvMmsghdrs[s].msg_len = 0; + } + pool->laid_vlen = vlen; + pool->laid_bufSz = bufSz; + pool->laid_ctrlSz = ctrlSz; + pool->used = 0; + } else { + unsigned int s; + const unsigned int used = pool->used; + for (s = 0; s < used; s++) { + recvMmsghdrs[s].msg_hdr.msg_namelen = addrLen; + recvMmsghdrs[s].msg_hdr.msg_control = recvCtrl + (s * ctrlSz); + recvMmsghdrs[s].msg_hdr.msg_controllen = ctrlSz; + recvMmsghdrs[s].msg_hdr.msg_flags = 0; + recvMmsghdrs[s].msg_len = 0; + } + } } ESOCK_CNT_INC(env, descP, sockRef, esock_atom_read_tries, &descP->readTries, 1); @@ -4239,6 +4268,8 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, readResult = sock_recvmmsg(descP->sock, recvMmsghdrs, vlen, flags, NULL); saveErrno = ESOCK_IS_ERROR(readResult) ? sock_errno() : 0; + pool->used = (readResult > 0) ? (unsigned int) readResult : 0; + if (readResult == 0) { ret = esock_make_ok2(env, MKEL(env)); goto cleanup; @@ -4311,8 +4342,8 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, cleanup: /* Free the binaries we still own; recv_create_bin may have handed some off - * (data == NULL -> FREE_BIN is a no-op). The bufs/ctrls arrays were zeroed - * above so slots the allocation loop never reached are skipped. */ + * (data == NULL -> FREE_BIN is a no-op). 'i' bounds countToFree to slots + * the allocation loop populated (0 on the empty/error paths). */ { unsigned int countToFree = (i < (unsigned int) readResult) ? i : (unsigned int) readResult; if (countToFree > 0) { diff --git a/lib/kernel/test/socket_SUITE.erl b/lib/kernel/test/socket_SUITE.erl index f0aef048023d..3684f6b60cb7 100644 --- a/lib/kernel/test/socket_SUITE.erl +++ b/lib/kernel/test/socket_SUITE.erl @@ -15814,8 +15814,8 @@ sendmmsg_dirty_scheduler_udp4(_Config) when is_list(_Config) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% Many recvmmsg calls on one socket, varying VLen/BufSz, to exercise the -%% recvmmsg scratch-pool reuse and grow paths. +%% Many recvmmsg calls on one socket, varying VLen/BufSz and received +%% count, to exercise the recvmmsg scratch-pool reuse/grow/reset paths. %% recvmmsg_pool_reuse_udp4(_Config) when is_list(_Config) -> ?TT(?SECS(60)), @@ -15842,6 +15842,13 @@ recvmmsg_pool_reuse_udp4(_Config) when is_list(_Config) -> recvmmsg_pool_reuse_round(S1, S2, VLen, BufSz) end, Rounds ++ Rounds), + %% Fixed layout, varying received count -> incremental reset, + %% incl. large-VLen/few-received. + lists:foreach( + fun(Count) -> + recvmmsg_pool_reuse_count(S1, S2, 64, 512, Count) + end, + [64, 1, 30, 64, 5, 1, 40, 64]), ok = socket:close(S1), ok = socket:close(S2), ok @@ -15849,11 +15856,15 @@ recvmmsg_pool_reuse_udp4(_Config) when is_list(_Config) -> ). recvmmsg_pool_reuse_round(S1, S2, VLen, BufSz) -> - Expected = [list_to_binary(io_lib:format("r~p_m~p", [VLen, N])) - || N <- lists:seq(1, VLen)], + recvmmsg_pool_reuse_count(S1, S2, VLen, BufSz, VLen). + +%% Send Count datagrams, receive with a recvmmsg of capacity VLen (Count =< VLen). +recvmmsg_pool_reuse_count(S1, S2, VLen, BufSz, Count) -> + Expected = [list_to_binary(io_lib:format("r~p_~p_m~p", [VLen, Count, N])) + || N <- lists:seq(1, Count)], lists:foreach(fun(D) -> ok = socket:send(S2, D) end, Expected), {ok, Received} = socket:recvmmsg(S1, VLen, BufSz, 0, [], infinity), - VLen = length(Received), + Count = length(Received), ReceivedData = [Data || Msg <- Received, [Data] <- [maps:get(iov, Msg)]], true = lists:sort(ReceivedData) =:= lists:sort(Expected), ok. From 06eb66831dbf120efed8ae623eaa129ca697240b Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 22 Jul 2026 18:22:27 +0200 Subject: [PATCH 3/3] erts: Right-size recvmmsg per-datagram output binaries The result loop allocated a full bufSz data binary and a full ctrlSz control binary for every datagram, then recv_create_bin realloced the data binary down to the payload length. For a data-only receiver (e.g. DNS) with the default 1024-byte control size and small payloads, that is a ~2 KB alloc + realloc-down plus a wasted ~1 KB control alloc per datagram -- the allocator was the largest remaining recv-side cost once the scratch pool removed the per-call churn. Since the payload and any cmsgs are copied out of the (reused) scratch block, allocate the output binaries at exactly the received sizes: msgLen for data and the actual control length for the control binary. The data binary is then handed off by recv_create_bin without a realloc, and a datagram with no ancillary data costs no control allocation at all. The result terms are identical; msg_control still points at the (now right-sized) control binary for cmsg decoding. --- erts/emulator/nifs/unix/unix_socket_syncio.c | 6 +-- lib/kernel/test/socket_SUITE.erl | 54 +++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/erts/emulator/nifs/unix/unix_socket_syncio.c b/erts/emulator/nifs/unix/unix_socket_syncio.c index d863d570be29..0da7833c4b3b 100644 --- a/erts/emulator/nifs/unix/unix_socket_syncio.c +++ b/erts/emulator/nifs/unix/unix_socket_syncio.c @@ -4297,16 +4297,14 @@ ERL_NIF_TERM essio_recvmmsg(ErlNifEnv* env, if (msgLen > bufSz) msgLen = bufSz; - ESOCK_ASSERT( ALLOC_BIN(bufSz, &bufs[i]) ); + ESOCK_ASSERT( ALLOC_BIN(msgLen, &bufs[i]) ); sys_memcpy(bufs[i].data, recvBufs + (i * bufSz), msgLen); - bufs[i].size = bufSz; - ESOCK_ASSERT( ALLOC_BIN(ctrlSz, &ctrls[i]) ); ctrlLen = (recvMmsghdrs[i].msg_hdr.msg_controllen < ctrlSz) ? recvMmsghdrs[i].msg_hdr.msg_controllen : ctrlSz; + ESOCK_ASSERT( ALLOC_BIN(ctrlLen, &ctrls[i]) ); sys_memcpy(ctrls[i].data, recvCtrl + (i * ctrlSz), ctrlLen); - ctrls[i].size = ctrlSz; recvMmsghdrs[i].msg_hdr.msg_control = ctrls[i].data; diff --git a/lib/kernel/test/socket_SUITE.erl b/lib/kernel/test/socket_SUITE.erl index 3684f6b60cb7..c2a5d3e14846 100644 --- a/lib/kernel/test/socket_SUITE.erl +++ b/lib/kernel/test/socket_SUITE.erl @@ -159,6 +159,7 @@ recvmmsg_dirty_scheduler_udp4/1, sendmmsg_dirty_scheduler_udp4/1, recvmmsg_pool_reuse_udp4/1, + recvmmsg_ctrl_udp4/1, %% Socket IOCTL simple ioctl_simple1/1, @@ -395,7 +396,8 @@ batch_cases() -> sendmmsg_invalid_msg_format, recvmmsg_dirty_scheduler_udp4, sendmmsg_dirty_scheduler_udp4, - recvmmsg_pool_reuse_udp4 + recvmmsg_pool_reuse_udp4, + recvmmsg_ctrl_udp4 ]. ioctl_cases() -> @@ -15870,6 +15872,56 @@ recvmmsg_pool_reuse_count(S1, S2, VLen, BufSz, Count) -> ok. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% recvmmsg with ancillary data: enable ip pktinfo on the receiver so each +%% datagram carries a control message, and verify recvmmsg decodes a +%% non-empty ctrl per message (exercises the ctrl output path with actual +%% cmsg bytes, not just the data-only empty-ctrl case). +%% +recvmmsg_ctrl_udp4(_Config) when is_list(_Config) -> + ?TT(?SECS(10)), + tc_try( + recvmmsg_ctrl_udp4, + fun() -> + has_support_ipv4(), + has_recvmmsg_support() + end, + fun() -> + {ok, S1} = socket:open(inet, dgram, udp), + {ok, S2} = socket:open(inet, dgram, udp), + {ok, Addr} = inet:getaddr("localhost", inet), + ok = socket:bind(S1, #{family => inet, addr => Addr, port => 0}), + {ok, #{port := LocalPort}} = socket:sockname(S1), + ok = socket:connect(S2, + #{family => inet, addr => Addr, + port => LocalPort}), + case socket:setopt(S1, ip, pktinfo, true) of + {error, _} -> + _ = socket:close(S1), + _ = socket:close(S2), + skip("ip pktinfo not supported"); + ok -> + N = 5, + lists:foreach( + fun(I) -> ok = socket:send(S2, integer_to_binary(I)) end, + lists:seq(1, N)), + {ok, Received} = socket:recvmmsg(S1, 10, 0, 0, [], infinity), + N = length(Received), + lists:foreach( + fun(#{ctrl := Ctrl}) -> + true = lists:any( + fun(#{level := ip, type := pktinfo}) -> true; + (_) -> false + end, Ctrl) + end, Received), + ok = socket:close(S1), + ok = socket:close(S2), + ok + end + end + ). + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Helper function to check if recvmmsg is supported %%