From 35980920a1fb7c92e0e3c4c48d480336cd5fe44a Mon Sep 17 00:00:00 2001 From: Rowan Claude Date: Thu, 29 Oct 2026 11:23:10 -0400 Subject: [PATCH] Socket buffers: keep the OS default rather than fail; netcode 1.4.9 A stock NetBSD 11.0 (kern.sbmax 256 KiB, about 227 KiB usable) refuses every buffer request down to netcode's old 256 KiB floor, so socket create failed and no server or client could start. A larger buffer is an optimisation, not a requirement: the back-off now halves down to 64 KiB, and if nothing is accepted the socket keeps the OS default. Found by yojimbo's NetBSD VM leg. test_socket_buffer_size_back_off covers it with a fake OS, no sockets; it fails with the old 256 KiB floor. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 2 +- netcode.c | 144 +++++++++++++++++++++++++++++++++++-------------- netcode.h | 4 +- 3 files changed, 108 insertions(+), 42 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a9921a..10ea1be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) set(CMAKE_TARGET_MESSAGES OFF) endif() -project(netcode VERSION 1.4.8 LANGUAGES C CXX) +project(netcode VERSION 1.4.9 LANGUAGES C CXX) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/netcode.c b/netcode.c index 6458cab..832960f 100755 --- a/netcode.c +++ b/netcode.c @@ -493,8 +493,6 @@ struct netcode_socket_holder_t #define NETCODE_SOCKET_ERROR_CREATE_FAILED 1 #define NETCODE_SOCKET_ERROR_SET_NON_BLOCKING_FAILED 2 #define NETCODE_SOCKET_ERROR_SOCKOPT_IPV6_ONLY_FAILED 3 -#define NETCODE_SOCKET_ERROR_SOCKOPT_RCVBUF_FAILED 4 -#define NETCODE_SOCKET_ERROR_SOCKOPT_SNDBUF_FAILED 5 #define NETCODE_SOCKET_ERROR_BIND_IPV4_FAILED 6 #define NETCODE_SOCKET_ERROR_BIND_IPV6_FAILED 7 #define NETCODE_SOCKET_ERROR_GET_SOCKNAME_IPV4_FAILED 8 @@ -502,6 +500,53 @@ struct netcode_socket_holder_t #define NETCODE_SOCKET_ERROR_DISABLE_UDP_PORT_CONNRESET_FAILED 10 #define NETCODE_SOCKET_ERROR_ENABLE_PACKET_TAGGING_FAILED 11 +#define NETCODE_SOCKET_BUFFER_SIZE_MINIMUM ( 64 * 1024 ) + +typedef int (*netcode_socket_buffer_size_try_t)( void * context, int size ); + +// linux and windows clamp a socket buffer request that exceeds the OS limit, but the BSDs reject it +// instead (a stock NetBSD rejects anything over about 227 KiB). halve the request until one is accepted. +// a larger buffer is an optimisation, never a requirement: if no request down to the minimum is accepted, +// the socket keeps the OS default. returns the size accepted, or 0 if the OS default was kept. + +static int netcode_socket_buffer_size_back_off( int requested_size, netcode_socket_buffer_size_try_t try_size, void * context ) +{ + for ( int size = requested_size; size >= NETCODE_SOCKET_BUFFER_SIZE_MINIMUM; size /= 2 ) + { + if ( try_size( context, size ) ) + return size; + } + return 0; +} + +struct netcode_socket_buffer_option_t +{ + netcode_socket_handle_t handle; + int option; +}; + +static int netcode_socket_buffer_size_set( void * context, int size ) +{ + struct netcode_socket_buffer_option_t * buffer_option = (struct netcode_socket_buffer_option_t*) context; + return setsockopt( buffer_option->handle, SOL_SOCKET, buffer_option->option, (char*)&size, sizeof(int) ) == 0; +} + +static void netcode_socket_increase_buffer_size( netcode_socket_handle_t handle, int option, int requested_size, const char * name ) +{ + struct netcode_socket_buffer_option_t buffer_option; + buffer_option.handle = handle; + buffer_option.option = option; + const int size = netcode_socket_buffer_size_back_off( requested_size, netcode_socket_buffer_size_set, &buffer_option ); + if ( size == 0 ) + { + netcode_printf( NETCODE_LOG_LEVEL_INFO, "socket %s buffer size kept at the OS default (%d was not accepted)\n", name, requested_size ); + } + else if ( size != requested_size ) + { + netcode_printf( NETCODE_LOG_LEVEL_INFO, "socket %s buffer size reduced from %d to %d\n", name, requested_size, size ); + } +} + void netcode_socket_destroy( struct netcode_socket_t * socket ) { netcode_assert( socket ); @@ -691,44 +736,10 @@ int netcode_socket_create( struct netcode_socket_t * s, struct netcode_address_t } } - // increase socket send and receive buffer sizes. linux and windows clamp requests that - // exceed the OS limit, but the BSDs reject them instead, so back off until accepted. + // increase socket send and receive buffer sizes (see netcode_socket_buffer_size_back_off) - { - int size = send_buffer_size; - while ( setsockopt( s->handle, SOL_SOCKET, SO_SNDBUF, (char*)&size, sizeof(int) ) != 0 ) - { - size /= 2; - if ( size < 256 * 1024 ) - { - netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to set socket send buffer size\n" ); - netcode_socket_destroy( s ); - return NETCODE_SOCKET_ERROR_SOCKOPT_SNDBUF_FAILED; - } - } - if ( size != send_buffer_size ) - { - netcode_printf( NETCODE_LOG_LEVEL_INFO, "socket send buffer size reduced from %d to %d\n", send_buffer_size, size ); - } - } - - { - int size = receive_buffer_size; - while ( setsockopt( s->handle, SOL_SOCKET, SO_RCVBUF, (char*)&size, sizeof(int) ) != 0 ) - { - size /= 2; - if ( size < 256 * 1024 ) - { - netcode_printf( NETCODE_LOG_LEVEL_ERROR, "error: failed to set socket receive buffer size\n" ); - netcode_socket_destroy( s ); - return NETCODE_SOCKET_ERROR_SOCKOPT_RCVBUF_FAILED; - } - } - if ( size != receive_buffer_size ) - { - netcode_printf( NETCODE_LOG_LEVEL_INFO, "socket receive buffer size reduced from %d to %d\n", receive_buffer_size, size ); - } - } + netcode_socket_increase_buffer_size( s->handle, SO_SNDBUF, send_buffer_size, "send" ); + netcode_socket_increase_buffer_size( s->handle, SO_RCVBUF, receive_buffer_size, "receive" ); // bind to port @@ -6006,6 +6017,60 @@ static void test_sequence() check( netcode_sequence_number_bytes_required( 0x1122334455667788 ) == 8 ); } +struct test_socket_buffer_os_t +{ + int limit; // the largest size this fake OS accepts (a BSD rejects anything above it) + int calls; + int smallest_tried; +}; + +static int test_socket_buffer_os_try( void * context, int size ) +{ + struct test_socket_buffer_os_t * os = (struct test_socket_buffer_os_t*) context; + os->calls++; + if ( os->smallest_tried == 0 || size < os->smallest_tried ) + os->smallest_tried = size; + return size <= os->limit; +} + +static void test_socket_buffer_size_back_off() +{ + const int requested = 4 * 1024 * 1024; + + // an OS that accepts the request (or clamps it, as linux and windows do) takes it on the first try + + { + struct test_socket_buffer_os_t os = { requested, 0, 0 }; + check( netcode_socket_buffer_size_back_off( requested, test_socket_buffer_os_try, &os ) == requested ); + check( os.calls == 1 ); + } + + // a stock NetBSD (kern.sbmax 256 KiB, about 227 KiB usable) refuses 4M, 2M, 1M, 512K and 256K, and takes 128K + + { + struct test_socket_buffer_os_t os = { 227 * 1024, 0, 0 }; + check( netcode_socket_buffer_size_back_off( requested, test_socket_buffer_os_try, &os ) == 128 * 1024 ); + check( os.calls == 6 ); + } + + // an OS that refuses every request down to the minimum keeps its default: not an error, and never a request below the minimum + + { + struct test_socket_buffer_os_t os = { 16 * 1024, 0, 0 }; + check( netcode_socket_buffer_size_back_off( requested, test_socket_buffer_os_try, &os ) == 0 ); + check( os.smallest_tried == NETCODE_SOCKET_BUFFER_SIZE_MINIMUM ); + check( os.calls == 7 ); + } + + // a request already below the minimum is not made at all + + { + struct test_socket_buffer_os_t os = { requested, 0, 0 }; + check( netcode_socket_buffer_size_back_off( 32 * 1024, test_socket_buffer_os_try, &os ) == 0 ); + check( os.calls == 0 ); + } +} + static void test_address() { { @@ -10539,6 +10604,7 @@ void netcode_test() RUN_TEST( test_endian ); RUN_TEST( test_address ); RUN_TEST( test_sequence ); + RUN_TEST( test_socket_buffer_size_back_off ); RUN_TEST( test_connect_token ); RUN_TEST( test_generate_connect_token_out_of_range ); RUN_TEST( test_challenge_token ); diff --git a/netcode.h b/netcode.h index c28d52f..e6d86f7 100755 --- a/netcode.h +++ b/netcode.h @@ -32,10 +32,10 @@ #ifndef NETCODE_H #define NETCODE_H -#define NETCODE_VERSION_FULL "1.4.8" +#define NETCODE_VERSION_FULL "1.4.9" #define NETCODE_VERSION_MAJOR 1 #define NETCODE_VERSION_MINOR 4 -#define NETCODE_VERSION_PATCH 8 +#define NETCODE_VERSION_PATCH 9 /* IMPORTANT: netcode is single-threaded by design and is not thread safe.