From 6de96e60db2ed4ff3b254df36dee5da19872c5a3 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Sun, 4 Oct 2026 02:39:15 -0700 Subject: [PATCH 1/7] netxduo: 6.5.2, the fork with upstream v6.5.2.202603_rel, and ALPN is the fork's third_party/netxduo 7bf58f91 -> cbba359a, tinic/netxduo master: the merge of PR #8 (the CertificateVerify scratch carved after the PRF region) on top of PR #7 (upstream v6.5.2.202603_rel, 20046b12, into the fork, on top of PR #6). AMINETXDUO_NETXDUO_VERSION_PIN 6.5.1 -> 6.5.2 in the same commit, as cmake/AmiNetXDuoVersion.cmake requires: configure stops when the pin and the submodule's nx_api.h disagree. The fork builds RFC 7301 ALPN itself (nx_secure/src/nx_secure_tls_alpn.c); its code is the same as src/tls/alpn/nx_secure_tls_alpn.c, and only the header comment differs. So, in this commit and not later: - src/tls/alpn/ is deleted; - src/tls/CMakeLists.txt no longer appends it to nx_secure (the glob of nx_secure/src picks up the fork's file); - tests/x509/CMakeLists.txt no longer appends it (same glob); - tests/fuzz/CMakeLists.txt: fuzz_tls_record lists the fork's file instead, and fuzz_tls_nx_secure no longer appends it (glob); - src/tlslib/CMakeLists.txt: test_tls_alpn builds the fork's file; - src/tlslib/test/test_tls_alpn.c: the header names the new path. No other place in the tree names src/tls/alpn. 20046b12 makes the CertificateVerify scratch per session: it is carved from the TLS metadata area, NX_SECURE_TLS_CERTIFICATE_VERIFY_SCRATCH_SIZE = 1364 bytes on m68k, and NX_SECURE_TLS_SESSION grows from 9876 to 9884 bytes. tls_conn.c sizes each connection's metadata with nx_secure_tls_metadata_size_calculate(), so nothing here changes for it; each TLS connection takes about 1.36 KB more. The fork's fragment budget (08b52c70) makes nx_ipv4_packet_receive.c read _nx_ip_created_count through NX_IP_FRAGMENT_ADMIT (nx_ip.h). Ten host fixtures compile that file from a hand-picked source list without nx_ip_create.c, and stopped linking: the fuzz targets dhcp_lease_regression, dhcp_lease_option_regression, fuzz_dhcp, fuzz_dns and fuzz_mdns (and, in the 32-bit build, the TLS fuzz targets that share fuzz_nxstub.c), and test_ipv6_frag, test_bcast_loopback, test_ipv4_noaddr_account, test_mcast_share_2sock and test_mcast_share_loopback. Each builds one NX_IP by hand, so each now defines the count, once per binary, as that one instance: in tests/fuzz/fuzz_nxstub.c and in the five host harnesses. The vendored tree and the shipping sources are unchanged. Two of those fixtures also needed the fork's current model to run: test_ipv6_frag's test_pool_reserve exercises the pool-wide reserve gate, which NX_IP_FRAGMENT_ADMIT applies only while another IP instance exists, so that test alone sets the count to 2 and restores it on the way out; and test_tcp_sws's blocked sender now waits on an H_MSS packet in tx_thread_additional_suspend_info, as nx_tcp_socket_send_internal.c stores it, because transmit_check (a820b430) measures the window against that packet. Co-Authored-By: Claude Opus 5.5 --- cmake/AmiNetXDuoVersion.cmake | 2 +- src/tls/CMakeLists.txt | 11 - src/tls/alpn/nx_secure_tls_alpn.c | 403 ------------------ src/tlslib/CMakeLists.txt | 2 +- src/tlslib/test/test_tls_alpn.c | 2 +- tests/fuzz/CMakeLists.txt | 11 +- tests/fuzz/fuzz_nxstub.c | 6 + tests/ipv6/host/test_ipv6_frag_host.c | 18 +- .../netstack/host/test_bcast_loopback_host.c | 6 + .../host/test_ipv4_noaddr_account_host.c | 6 + .../host/test_mcast_share_2sock_host.c | 6 + .../host/test_mcast_share_loopback_host.c | 6 + tests/netstack/host/test_tcp_sws_host.c | 9 + tests/x509/CMakeLists.txt | 5 - third_party/netxduo | 2 +- 15 files changed, 62 insertions(+), 433 deletions(-) delete mode 100644 src/tls/alpn/nx_secure_tls_alpn.c diff --git a/cmake/AmiNetXDuoVersion.cmake b/cmake/AmiNetXDuoVersion.cmake index 94343e837..fb23ecde7 100644 --- a/cmake/AmiNetXDuoVersion.cmake +++ b/cmake/AmiNetXDuoVersion.cmake @@ -28,7 +28,7 @@ # What third_party/ is expected to contain. These are the ONLY hardcoded # upstream version numbers in the tree. Bumping a submodule means editing the # matching line here, in the same commit, on purpose. -set(AMINETXDUO_NETXDUO_VERSION_PIN "6.5.1") +set(AMINETXDUO_NETXDUO_VERSION_PIN "6.5.2") set(AMINETXDUO_THREADX_VERSION_PIN "6.5.2") # --------------------------------------------------------------- reading ---- diff --git a/src/tls/CMakeLists.txt b/src/tls/CMakeLists.txt index 28aad08fc..6be852dd7 100644 --- a/src/tls/CMakeLists.txt +++ b/src/tls/CMakeLists.txt @@ -78,17 +78,6 @@ list(APPEND NX_SECURE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/rfc7905/nx_secure_tls_record_payload_encrypt.c" "${CMAKE_CURRENT_SOURCE_DIR}/rfc7905/nx_secure_tls_record_payload_decrypt.c") -# RFC 7301, ALPN. nx_secure has none, which is why nothing built on it could -# negotiate HTTP/2: h2 over TLS is defined only over a negotiated "h2" -# (RFC 7540 3.2), never by assumption. This adds it rather than replacing -# anything, so it is an append and not an exclusion. It lives here and not in -# third_party/ so that a submodule bump is a merge of the seven one-line call -# sites in nx_secure and nothing else; the ProtocolNameList field in -# NX_SECURE_TLS_SESSION is the one thing that cannot live outside the vendored -# header. -list(APPEND NX_SECURE_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/alpn/nx_secure_tls_alpn.c") - add_library(nx_secure STATIC ${NX_SECURE_SOURCES}) target_include_directories(nx_secure PUBLIC ${AMINETXDUO_TLS_INCLUDES}) target_link_libraries(nx_secure PUBLIC nx_crypto netxduo) diff --git a/src/tls/alpn/nx_secure_tls_alpn.c b/src/tls/alpn/nx_secure_tls_alpn.c deleted file mode 100644 index b467eeaf2..000000000 --- a/src/tls/alpn/nx_secure_tls_alpn.c +++ /dev/null @@ -1,403 +0,0 @@ -/* - * RFC 7301, Application-Layer Protocol Negotiation, for nx_secure. - * - * nx_secure has no ALPN of any kind, which is why nothing built on it can - * speak HTTP/2: h2 over TLS is defined only over a negotiated "h2", never by - * assumption (RFC 7540 3.2). This file is the whole of the mechanism. The - * seven call sites in third_party/netxduo/nx_secure are one line each, on - * purpose: it keeps a submodule bump a merge of seven single lines rather than - * of a feature. - * - * It is built into the nx_secure archive alongside the vendored sources, the - * same way src/tls/rfc7905/ supplies the two ChaCha20-Poly1305 record files. - * - * THE WIRE SHAPE, both directions: - * - * ext_type (2) | ext_length (2) | list_length (2) | ProtocolName... - * ProtocolName = length (1) | bytes - * - * The client sends its whole preference list. The server answers with a list - * of exactly one, which is the selection (RFC 7301 3.1); in TLS 1.2 that rides - * in the ServerHello and in TLS 1.3 in EncryptedExtensions (RFC 8446 4.2). - * - * A server with no ALPN list may ignore the offer. A configured server with - * no protocol in common MUST instead send fatal no_application_protocol per - * RFC 7301 3.2; likewise a client refuses an unoffered selection. - * - * SPDX-License-Identifier: MIT - */ - -#define NX_SECURE_SOURCE_CODE - -#include "nx_secure_tls.h" - -/* - * Walk a ProtocolNameList and answer whether it is well formed: a run of - * length-prefixed names that ends exactly on `length`, with no empty name - * (RFC 7301 3.1 makes ProtocolName opaque<1..2^8-1>). - */ -static UINT nx_secure_tls_alpn_list_valid(const UCHAR *list, UINT length) -{ -UINT offset = 0; - - if (list == NX_NULL) - { - return(NX_FALSE); - } - - while (offset < length) - { - UINT name_length = list[offset]; - - if (name_length == 0) - { - return(NX_FALSE); - } - - offset += 1u + name_length; - } - - return((offset == length) ? NX_TRUE : NX_FALSE); -} - -/* TRUE when `name` appears in the wire-encoded `list`. */ -static UINT nx_secure_tls_alpn_list_has(const UCHAR *list, UINT list_length, - const UCHAR *name, UINT name_length) -{ -UINT offset = 0; - - while ((offset + 1u + name_length) <= list_length) - { - UINT entry_length = list[offset]; - - if ((entry_length == name_length) && - (NX_SECURE_MEMCMP(&list[offset + 1], name, name_length) == 0)) - { - return(NX_TRUE); - } - - offset += 1u + entry_length; - } - - return(NX_FALSE); -} - -/**************************************************************************/ -/* _nx_secure_tls_alpn_protocol_set */ -/* */ -/* Offer `protocol_list`, the wire encoding of ProtocolNameList without */ -/* its outer length: "\x02h2\x08http/1.1". The caller keeps ownership */ -/* and the buffer must outlive the handshake. A NULL list clears the */ -/* offer, and the extension is then not sent at all. */ -/**************************************************************************/ -UINT _nx_secure_tls_alpn_protocol_set(NX_SECURE_TLS_SESSION *tls_session, - const UCHAR *protocol_list, - USHORT protocol_list_length) -{ -UINT offset; - - if (tls_session == NX_NULL) - { - return(NX_PTR_ERROR); - } - - if ((protocol_list == NX_NULL) || (protocol_list_length == 0)) - { - tls_session -> nx_secure_tls_alpn_protocol_list = NX_NULL; - tls_session -> nx_secure_tls_alpn_protocol_list_length = 0; - return(NX_SUCCESS); - } - - /* Checked here rather than on the wire: a malformed list written into a - ClientHello is a malformed ClientHello, and the server's answer to that - is a handshake failure with no explanation. */ - if (!nx_secure_tls_alpn_list_valid(protocol_list, protocol_list_length)) - { - return(NX_SECURE_TLS_INVALID_PACKET); - } - - /* Local selections have a fixed-size destination. Reject an offer we - could send but could never accept back from a conforming peer. Keep - the wire validator above RFC-sized: a remote name may be longer. */ - for (offset = 0; offset < protocol_list_length; - offset += 1u + protocol_list[offset]) - { - if (protocol_list[offset] > NX_SECURE_TLS_ALPN_PROTOCOL_MAX) - { - return(NX_SECURE_TLS_INVALID_PACKET); - } - } - - tls_session -> nx_secure_tls_alpn_protocol_list = protocol_list; - tls_session -> nx_secure_tls_alpn_protocol_list_length = protocol_list_length; - - return(NX_SUCCESS); -} - -/**************************************************************************/ -/* _nx_secure_tls_alpn_protocol_get */ -/* */ -/* What was negotiated, or NX_SECURE_TLS_EXTENSION_NOT_FOUND when the */ -/* peer did not answer. Not NUL terminated: ProtocolName is opaque. */ -/**************************************************************************/ -UINT _nx_secure_tls_alpn_protocol_get(NX_SECURE_TLS_SESSION *tls_session, - const UCHAR **protocol, - UCHAR *protocol_length) -{ - if ((tls_session == NX_NULL) || (protocol == NX_NULL) || - (protocol_length == NX_NULL)) - { - return(NX_PTR_ERROR); - } - - if (tls_session -> nx_secure_tls_alpn_selected_length == 0) - { - *protocol = NX_NULL; - *protocol_length = 0; - return(NX_SECURE_TLS_EXTENSION_NOT_FOUND); - } - - *protocol = tls_session -> nx_secure_tls_alpn_selected; - *protocol_length = tls_session -> nx_secure_tls_alpn_selected_length; - - return(NX_SUCCESS); -} - -/**************************************************************************/ -/* _nx_secure_tls_alpn_send_extension */ -/* */ -/* Write the extension at *packet_offset. On a client that is the whole */ -/* offer; on a server it is the one selected name, and nothing at all if */ -/* no name was selected. *extension_length is zero when nothing was */ -/* written, which is what the callers add to their running total. */ -/**************************************************************************/ -UINT _nx_secure_tls_alpn_send_extension(NX_SECURE_TLS_SESSION *tls_session, - UCHAR *packet_buffer, ULONG *packet_offset, - USHORT *extension_length, - ULONG available_size, UINT server) -{ -ULONG offset; -const UCHAR *list; -UINT list_length; -UCHAR one[NX_SECURE_TLS_ALPN_PROTOCOL_MAX + 1]; - - if ((tls_session == NX_NULL) || (packet_buffer == NX_NULL) || - (packet_offset == NX_NULL) || (extension_length == NX_NULL)) - { - return(NX_PTR_ERROR); - } - - *extension_length = 0; - - if (server) - { - if (tls_session -> nx_secure_tls_alpn_selected_length == 0) - { - - /* Nothing was selected, so nothing is answered. RFC 7301 3.2 - allows the server to stay silent, and the connection then has no - agreed application protocol. */ - return(NX_SUCCESS); - } - - one[0] = tls_session -> nx_secure_tls_alpn_selected_length; - NX_SECURE_MEMCPY(&one[1], tls_session -> nx_secure_tls_alpn_selected, - tls_session -> nx_secure_tls_alpn_selected_length); /* Use case of memcpy is verified. */ - - list = one; - list_length = 1u + tls_session -> nx_secure_tls_alpn_selected_length; - } - else - { - if (tls_session -> nx_secure_tls_alpn_protocol_list_length == 0) - { - return(NX_SUCCESS); - } - - list = tls_session -> nx_secure_tls_alpn_protocol_list; - list_length = tls_session -> nx_secure_tls_alpn_protocol_list_length; - } - - offset = *packet_offset; - - /* ext_type, ext_length, list_length, then the names. */ - if (available_size < (offset + 6u + list_length)) - { - return(NX_SECURE_TLS_PACKET_BUFFER_TOO_SMALL); - } - - packet_buffer[offset] = (UCHAR)((NX_SECURE_TLS_EXTENSION_ALPN & 0xFF00) >> 8); - packet_buffer[offset + 1] = (UCHAR)(NX_SECURE_TLS_EXTENSION_ALPN & 0x00FF); - offset += 2; - - packet_buffer[offset] = (UCHAR)(((list_length + 2u) & 0xFF00) >> 8); - packet_buffer[offset + 1] = (UCHAR)((list_length + 2u) & 0x00FF); - offset += 2; - - packet_buffer[offset] = (UCHAR)((list_length & 0xFF00) >> 8); - packet_buffer[offset + 1] = (UCHAR)(list_length & 0x00FF); - offset += 2; - - NX_SECURE_MEMCPY(&packet_buffer[offset], list, list_length); /* Use case of memcpy is verified. */ - offset += list_length; - - *extension_length = (USHORT)(offset - *packet_offset); - *packet_offset = offset; - - return(NX_SUCCESS); -} - -/**************************************************************************/ -/* _nx_secure_tls_alpn_process_response */ -/* */ -/* The client half. `packet_buffer` starts at the extension's own */ -/* two-byte length field and `message_length` is what remains of the */ -/* message, so this validates its own bounds. */ -/**************************************************************************/ -UINT _nx_secure_tls_alpn_process_response(NX_SECURE_TLS_SESSION *tls_session, - const UCHAR *packet_buffer, - UINT message_length) -{ -UINT ext_length; -UINT list_length; -UINT name_length; - - if ((tls_session == NX_NULL) || (packet_buffer == NX_NULL)) - { - return(NX_PTR_ERROR); - } - - if (message_length < 2u) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - ext_length = (UINT)((packet_buffer[0] << 8) + packet_buffer[1]); - if ((ext_length + 2u) > message_length) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - /* list_length (2) + name_length (1) + at least one byte of name. */ - if (ext_length < 4u) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - list_length = (UINT)((packet_buffer[2] << 8) + packet_buffer[3]); - if (list_length != (ext_length - 2u)) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - name_length = packet_buffer[4]; - if ((name_length == 0) || ((name_length + 1u) != list_length)) - { - - /* RFC 7301 3.1: the server's list is exactly one name. Two names is - not a longer answer, it is a message this code cannot act on. */ - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - if (name_length > NX_SECURE_TLS_ALPN_PROTOCOL_MAX) - { - return(NX_SECURE_TLS_ALPN_PROTOCOL_MISMATCH); - } - - /* - * RFC 7301 3.2, and this is the check that matters. A server that selects - * a protocol the client never offered has answered a question that was not - * asked, and a client that accepts it then speaks a protocol it may not - * implement over an authenticated channel. - */ - if (!nx_secure_tls_alpn_list_has(tls_session -> nx_secure_tls_alpn_protocol_list, - tls_session -> nx_secure_tls_alpn_protocol_list_length, - &packet_buffer[5], name_length)) - { - return(NX_SECURE_TLS_ALPN_PROTOCOL_MISMATCH); - } - - NX_SECURE_MEMCPY(tls_session -> nx_secure_tls_alpn_selected, - &packet_buffer[5], name_length); /* Use case of memcpy is verified. */ - tls_session -> nx_secure_tls_alpn_selected_length = (UCHAR)name_length; - - return(NX_SUCCESS); -} - -/**************************************************************************/ -/* _nx_secure_tls_alpn_process_offer */ -/* */ -/* The server half. Selects the FIRST of our own protocols that the */ -/* client also offered, so the preference is the server's (RFC 7301 3.2 */ -/* leaves the choice to the server and warns against following the */ -/* client's order). A configured list without overlap is fatal. */ -/**************************************************************************/ -UINT _nx_secure_tls_alpn_process_offer(NX_SECURE_TLS_SESSION *tls_session, - const UCHAR *packet_buffer, - UINT message_length) -{ -UINT ext_length; -UINT list_length; -UINT offset; -const UCHAR *ours; -UINT ours_length; - - if ((tls_session == NX_NULL) || (packet_buffer == NX_NULL)) - { - return(NX_PTR_ERROR); - } - - tls_session -> nx_secure_tls_alpn_selected_length = 0; - - ours = tls_session -> nx_secure_tls_alpn_protocol_list; - ours_length = tls_session -> nx_secure_tls_alpn_protocol_list_length; - - if (message_length < 2u) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - ext_length = (UINT)((packet_buffer[0] << 8) + packet_buffer[1]); - if (((ext_length + 2u) > message_length) || (ext_length < 4u)) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - list_length = (UINT)((packet_buffer[2] << 8) + packet_buffer[3]); - if (list_length != (ext_length - 2u)) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - if (!nx_secure_tls_alpn_list_valid(&packet_buffer[4], list_length)) - { - return(NX_SECURE_TLS_INCORRECT_MESSAGE_LENGTH); - } - - if ((ours == NX_NULL) || (ours_length == 0)) - { - return(NX_SUCCESS); - } - - for (offset = 0; offset < ours_length; offset += 1u + ours[offset]) - { - UINT name_length = ours[offset]; - - if (name_length > NX_SECURE_TLS_ALPN_PROTOCOL_MAX) - { - continue; - } - - if (nx_secure_tls_alpn_list_has(&packet_buffer[4], list_length, - &ours[offset + 1], name_length)) - { - NX_SECURE_MEMCPY(tls_session -> nx_secure_tls_alpn_selected, - &ours[offset + 1], name_length); /* Use case of memcpy is verified. */ - tls_session -> nx_secure_tls_alpn_selected_length = (UCHAR)name_length; - break; - } - } - - return((tls_session -> nx_secure_tls_alpn_selected_length != 0) - ? NX_SUCCESS : NX_SECURE_TLS_ALPN_PROTOCOL_MISMATCH); -} diff --git a/src/tlslib/CMakeLists.txt b/src/tlslib/CMakeLists.txt index e0bc74641..855366882 100644 --- a/src/tlslib/CMakeLists.txt +++ b/src/tlslib/CMakeLists.txt @@ -519,7 +519,7 @@ if(NOT CMAKE_CROSSCOMPILING) add_executable(test_tls_alpn test/test_tls_alpn.c tls_alpn.c - "${CMAKE_SOURCE_DIR}/src/tls/alpn/nx_secure_tls_alpn.c" + "${AMINETXDUO_NETXDUO}/nx_secure/src/nx_secure_tls_alpn.c" "${AMINETXDUO_NETXDUO}/nx_secure/src/nx_secure_tls_map_error_to_alert.c") add_executable(test_tls_transport test/test_tls_transport.c diff --git a/src/tlslib/test/test_tls_alpn.c b/src/tlslib/test/test_tls_alpn.c index 59ff5ff55..1c26d1c3c 100644 --- a/src/tlslib/test/test_tls_alpn.c +++ b/src/tlslib/test/test_tls_alpn.c @@ -1,5 +1,5 @@ /* - * RFC 7301 ALPN, src/tls/alpn/nx_secure_tls_alpn.c and src/tlslib/tls_alpn.c. + * RFC 7301 ALPN, third_party/netxduo/nx_secure/src/nx_secure_tls_alpn.c and src/tlslib/tls_alpn.c. * * nx_secure had no ALPN at all, so nothing built on it could negotiate * HTTP/2: h2 over TLS is defined only over a negotiated "h2" (RFC 7540 3.2). diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt index cb0735be9..618f9e8cd 100644 --- a/tests/fuzz/CMakeLists.txt +++ b/tests/fuzz/CMakeLists.txt @@ -404,9 +404,8 @@ add_executable(fuzz_tls_record # RFC 7301. The ServerHello extension walk reaches it, so the ALPN # response parser is fuzzed by the same corpus rather than needing one of # its own: it is length-prefixed bytes off the wire like everything else - # in this target. src/tls/CMakeLists.txt says why it lives outside - # third_party. - "${CMAKE_SOURCE_DIR}/src/tls/alpn/nx_secure_tls_alpn.c" + # in this target. + "${_fuzz_secure_src}/nx_secure_tls_alpn.c" "${_fuzz_secure_src}/nx_secure_tls_process_certificate_request.c" "${_fuzz_secure_src}/nx_secure_tls_ciphersuite_lookup.c" "${_fuzz_secure_src}/nx_secure_tls_check_protocol_version.c" @@ -497,14 +496,8 @@ if(CMAKE_SIZEOF_VOID_P EQUAL 4) list(FILTER _fuzz_secure_all EXCLUDE REGEX "/nx_secure_tls_record_payload_(en|de)crypt\\.c$") - # RFC 7301. The vendored tree declares _nx_secure_tls_alpn_* and calls - # them from the ClientHello extension walk, but ships no implementation -- - # ours is the only one, and it lives outside third_party for the reason - # src/tls/CMakeLists.txt gives. Without it the archive's own callers are - # unresolved. add_library(fuzz_tls_nx_secure STATIC ${_fuzz_secure_all} - "${CMAKE_SOURCE_DIR}/src/tls/alpn/nx_secure_tls_alpn.c" "${CMAKE_SOURCE_DIR}/src/tls/rfc7905/nx_secure_tls_record_payload_encrypt.c" "${CMAKE_SOURCE_DIR}/src/tls/rfc7905/nx_secure_tls_record_payload_decrypt.c") diff --git a/tests/fuzz/fuzz_nxstub.c b/tests/fuzz/fuzz_nxstub.c index 0a0ce0abc..02bb24f03 100644 --- a/tests/fuzz/fuzz_nxstub.c +++ b/tests/fuzz/fuzz_nxstub.c @@ -16,6 +16,12 @@ #include "nx_api.h" +/* NX_IP_FRAGMENT_ADMIT (nx_ip.h) reads the count nx_ip_create() keeps. No + binary that links this file links nx_ip_create.c; each builds at most one + NX_IP by hand, so the count is defined here as that one instance. */ +ULONG _nx_ip_created_count = 1; + + UINT _tx_mutex_get(TX_MUTEX *mutex_ptr, ULONG wait_option) { diff --git a/tests/ipv6/host/test_ipv6_frag_host.c b/tests/ipv6/host/test_ipv6_frag_host.c index 8c48e7338..79684ddeb 100644 --- a/tests/ipv6/host/test_ipv6_frag_host.c +++ b/tests/ipv6/host/test_ipv6_frag_host.c @@ -144,6 +144,12 @@ VOID _nx_icmpv4_send_error_message(NX_IP *ip_ptr, NX_PACKET *offending_packet, static NX_IP h_ip; static NX_PACKET_POOL h_pool; + +/* NX_IP_FRAGMENT_ADMIT (nx_ip.h) reads the count nx_ip_create() keeps, and + this fixture builds its one NX_IP by hand without nx_ip_create.c, so the + count is defined here as that one instance. */ +ULONG _nx_ip_created_count = 1; + static NX_PACKET h_packet[H_PACKETS]; static UCHAR h_body[H_PACKETS][H_PAYLOAD]; static NX_IPV6_HEADER h_v6_header[H_PACKETS]; @@ -355,7 +361,15 @@ static VOID test_reassembles_out_of_order(VOID) static VOID test_pool_reserve(VOID) { -UINT status; +UINT status; +ULONG ip_count; + + /* The pool-wide reserve is the gate for a pool another IP instance may + share: NX_IP_FRAGMENT_ADMIT applies it only while more than one NX_IP + exists. Model that second, live instance for this test alone, and + give the one-instance count back on the way out. */ + ip_count = _nx_ip_created_count; + _nx_ip_created_count = 2; h_reset(); @@ -386,6 +400,8 @@ UINT status; h_check(status != NX_SUCCESS, "a fragment below the reserve is refused"); h_check(h_ip.nx_ip_received_fragment_head == NX_NULL, "and is queued nowhere either"); + + _nx_ip_created_count = ip_count; } static VOID test_incomplete_times_out(VOID) diff --git a/tests/netstack/host/test_bcast_loopback_host.c b/tests/netstack/host/test_bcast_loopback_host.c index 4898d8cdb..6dfc9adfe 100644 --- a/tests/netstack/host/test_bcast_loopback_host.c +++ b/tests/netstack/host/test_bcast_loopback_host.c @@ -38,6 +38,12 @@ static void h_check(int ok, const char *what) static NX_IP h_ip; static NX_PACKET_POOL h_pool; + +/* NX_IP_FRAGMENT_ADMIT (nx_ip.h) reads the count nx_ip_create() keeps, and + this fixture builds its one NX_IP by hand without nx_ip_create.c, so the + count is defined here as that one instance. */ +ULONG _nx_ip_created_count = 1; + static NX_UDP_SOCKET h_server; /* bound to H_SERVER_PORT */ static NX_UDP_SOCKET h_client; /* the one that broadcasts */ diff --git a/tests/netstack/host/test_ipv4_noaddr_account_host.c b/tests/netstack/host/test_ipv4_noaddr_account_host.c index 5ed45b377..564ecd773 100644 --- a/tests/netstack/host/test_ipv4_noaddr_account_host.c +++ b/tests/netstack/host/test_ipv4_noaddr_account_host.c @@ -46,6 +46,12 @@ static void h_check(int ok, const char *what) static unsigned long h_releases; + +/* NX_IP_FRAGMENT_ADMIT (nx_ip.h) reads the count nx_ip_create() keeps, and + h_run() builds its one NX_IP on the stack without nx_ip_create.c, so the + count is defined here as that one instance. */ +ULONG _nx_ip_created_count = 1; + VOID _nx_packet_release(NX_PACKET *packet_ptr) { (void)packet_ptr; diff --git a/tests/netstack/host/test_mcast_share_2sock_host.c b/tests/netstack/host/test_mcast_share_2sock_host.c index fa2451cf5..0655b3c5a 100644 --- a/tests/netstack/host/test_mcast_share_2sock_host.c +++ b/tests/netstack/host/test_mcast_share_2sock_host.c @@ -40,6 +40,12 @@ static void h_check(int ok, const char *what) static NX_IP h_ip; static NX_PACKET_POOL h_pool; + +/* NX_IP_FRAGMENT_ADMIT (nx_ip.h) reads the count nx_ip_create() keeps, and + this fixture builds its one NX_IP by hand without nx_ip_create.c, so the + count is defined here as that one instance. */ +ULONG _nx_ip_created_count = 1; + static NX_UDP_SOCKET h_a; /* the sender, bound first */ static NX_UDP_SOCKET h_b; /* the co-bound sibling */ diff --git a/tests/netstack/host/test_mcast_share_loopback_host.c b/tests/netstack/host/test_mcast_share_loopback_host.c index 260bf8d66..417c01de4 100644 --- a/tests/netstack/host/test_mcast_share_loopback_host.c +++ b/tests/netstack/host/test_mcast_share_loopback_host.c @@ -41,6 +41,12 @@ static void h_check(int ok, const char *what) static NX_IP h_ip; static NX_PACKET_POOL h_pool; + +/* NX_IP_FRAGMENT_ADMIT (nx_ip.h) reads the count nx_ip_create() keeps, and + this fixture builds its one NX_IP by hand without nx_ip_create.c, so the + count is defined here as that one instance. */ +ULONG _nx_ip_created_count = 1; + static NX_UDP_SOCKET h_m; /* mDNS responder, bound first */ static NX_UDP_SOCKET h_a; /* the sender */ static NX_UDP_SOCKET h_b; /* the co-bound sibling */ diff --git a/tests/netstack/host/test_tcp_sws_host.c b/tests/netstack/host/test_tcp_sws_host.c index 53c03f8a2..67cc85f1b 100644 --- a/tests/netstack/host/test_tcp_sws_host.c +++ b/tests/netstack/host/test_tcp_sws_host.c @@ -267,6 +267,7 @@ static NX_IP h_ip; static NX_INTERFACE h_iface; static NX_TCP_SOCKET h_sock; static TX_THREAD h_waiter; +static NX_PACKET h_waiting_write; /* what h_waiter is blocked on */ static NX_PACKET h_pkt[H_PACKETS]; static UCHAR h_pkt_buf[H_PACKETS][H_BUF]; @@ -277,6 +278,7 @@ static void h_fixture(void) memset(&h_ip, 0, sizeof(h_ip)); memset(&h_iface, 0, sizeof(h_iface)); memset(&h_waiter, 0, sizeof(h_waiter)); + memset(&h_waiting_write, 0, sizeof(h_waiting_write)); h_now = 1000; h_datagrams = 0; @@ -506,6 +508,13 @@ static void g_congestion_window_is_not_a_sliver(void) static void i_a_blocked_sender_is_woken_once(void) { h_fixture(); + + /* A sender blocked in nx_tcp_socket_send() waits with its packet in + tx_thread_additional_suspend_info (nx_tcp_socket_send_internal.c), and + transmit_check measures the window against that packet. A full + segment of data, as a bulk writer would have queued. */ + h_waiting_write.nx_packet_length = H_MSS; + h_waiter.tx_thread_additional_suspend_info = (VOID *)&h_waiting_write; h_sock.nx_tcp_socket_transmit_suspension_list = &h_waiter; h_sock.nx_tcp_socket_transmit_suspended_count = 1; h_in_flight(512, 200); diff --git a/tests/x509/CMakeLists.txt b/tests/x509/CMakeLists.txt index 5775ee10c..5fd7e49ec 100644 --- a/tests/x509/CMakeLists.txt +++ b/tests/x509/CMakeLists.txt @@ -37,11 +37,6 @@ list(FILTER _x509_secure_all EXCLUDE REGEX file(GLOB _x509_crypto_all "${_x509_crypto_src}/*.c") list(FILTER _x509_crypto_all EXCLUDE REGEX "self_test") -# RFC 7301. src/tls/CMakeLists.txt appends the same file to the shipping -# nx_secure archive; the ClientHello and ServerHello extension builders call -# into it, so an archive without it does not link. -list(APPEND _x509_secure_all "${CMAKE_SOURCE_DIR}/src/tls/alpn/nx_secure_tls_alpn.c") - add_library(test_x509_nx_secure STATIC ${_x509_secure_all}) add_library(test_x509_nx_crypto STATIC ${_x509_crypto_all}) diff --git a/third_party/netxduo b/third_party/netxduo index 7bf58f91a..cbba359ad 160000 --- a/third_party/netxduo +++ b/third_party/netxduo @@ -1 +1 @@ -Subproject commit 7bf58f91a53ad6d73ccad2da7edfbaaac21f430f +Subproject commit cbba359adb35e60df49cb1ad94839ba06e6494ac From ee747e78c923d433b2f4e28b5455becfd74851da Mon Sep 17 00:00:00 2001 From: turo Date: Sat, 3 Oct 2026 18:18:10 -0700 Subject: [PATCH 2/7] tests/syncache: the SYN cache's contract at the fork's PR #6 tip Test-only. At a5cb72dd the tests/syncache arms are written against netxduo 7bf58f91. Against the pin bump (e5e89f1b) the two executables did not link: nx_tcp_syncache.c calls _nx_tcp_packet_send_ack, _nx_tcp_socket_packet_process and _nx_packet_release since 96502647 and 2d44d563. With those supplied, 21 cache assertions failed, detach keep crashed (rc 139) and v6accept failed. Each failure was run at every SYN-cache commit after the pin (8a507baf 1b57d8f0 ac17df7f 96502647 2d44d563 8babcaca edf44b07 5523e5bc, then 401cd686 and e5e89f1b) to find where it starts. Every one starts at a commit that changed the behaviour on purpose; none is a NetX defect. 96502647 "the SYN cache answers a SYN only when a socket can take it": "A SYN that finds no socket parked is now recorded as NX_TCP_SYNCACHE_DEFERRED and not answered." The arms that built a finished handshake with no socket (SYN then ACK, none parked) now get a deferred SYN, an ACK the cache drops ("Nothing was sent for this SYN"), and an empty accept queue. The commit says how one arises now: "Two SYNs that arrive while one socket is parked are both answered ... so the second can still finish with no socket." The arms are rebuilt that way. A full cache with no socket parked is reached by filling it while one is parked, since "Deferred SYNs and finished handshakes together are bounded by the listen backlog". 15 cache assertions: a full backlog waits; past the backlog the queue does not grow; and the peer is told; relisten takes one; the queue shortens; and the connection reaches the application; and resets every peer waiting on it (unlisten, finished handshakes); the cookie SYN-ACK was sent with no socket on the port; and it announced a window scale; and the ACK reconstructs that scale; one finished handshake is waiting; a forged SYN on the same four-tuple does not throw it away; in the accept queue; a relisten takes it; with the round trip of the handshake, not of the wait. The same cause gives detach v6accept ("the handshake is queued for accept") and detach keep: its queued handshake never existed, so the survivors check read nx_tcp_syncache_accept_head -> nx_tcp_syncache_interface through a NULL head in the test. That NULL read is the rc 139. It is a test harness fault, not a production memory access. The check now reports a NULL head as a failure instead of reading through it. Four assertions that passed at these commits passed vacuously. Each "a cookie completes" with no socket parked was a deferred SYN whose ACK the cache drops and reports as consumed. Each one now also requires cookies_valid to move. 2d44d563 "a connection the SYN cache finishes waits for accept on the socket": "Any other socket, at the ACK or on relisten, is now left as upstream left it" (bound, LISTEN, the peer's port), and "nx_tcp_server_socket_accept() ... completes the connection instead (_nx_tcp_syncache_accept)". 4 cache assertions: the connection is established (twice: the cached and the cookie handshake); and it is the socket that was parked; and the connection is made. They now assert the socket waits for accept, sends nothing, and that _nx_tcp_syncache_accept connects it with one ACK from iss + 1 acknowledging irs + 1. A new block holds data before accept: in-order data is held unacknowledged, a segment that does not follow it is released, and accept processes the held data in order. e5e89f1b "a cached SYN-ACK's sequence number never decodes as a cookie": the expiry block (an ACK for an entry that has expired) is inverted, not deleted. The ACK is now not consumed (the caller resets it), is not read as a cookie (cookies_valid 0, cookies_invalid 1), makes no connection, rebuilds nothing onto the parked socket, and the cache sends nothing for it. The RST itself, , is formed by _nx_tcp_packet_process, which this rig does not link. The fork's netx_3_06, 8_02 and 10_23_01 and ctl_syncookie check that RST. A fresh SYN from the same peer is then answered with a new sequence number and completes; full-table recovery by cookie is checked in the cookie blocks. 8a507baf, 1b57d8f0, ac17df7f, 8babcaca, edf44b07 and 5523e5bc change no assertion here. The three functions: - test_syncache models them and drives each one. _nx_tcp_packet_send_ack records the socket, sequence, acknowledgment and window, and fails on a socket with no peer. _nx_tcp_socket_packet_process takes a held packet as an ESTABLISHED socket takes in-order data: it fails on a packet the cache does not own, one still marked queued, a socket not established, or data out of order. _nx_packet_release fails on a packet the rig did not give out or that was already released. A model failure fails the arm. - test_syncache_detach: no arm reaches accept, hold or the accept-queue data ACK, so _nx_tcp_socket_packet_process and _nx_tcp_packet_send_ack abort the test if called. Its _nx_packet_release is unchanged. Boundary vectors in the cookie arm: - the counter at 0xFFFFFFFF is accepted in its own step and at 0 (and at top + 1 in ULONG arithmetic), and refused at 1 (top + 2) and 0xFFFFFFFE; - the counter at 0xFF is accepted at 0x100 and refused at 0x101; - the additive carry: at counter 12345 and at 0xFFFFFFFF, a searched irs for which h1 + irs overflows 32 bits and (h2 & 0xFFFFFF) + 0x3FF crosses 2^24. The cookie built with data 0x3FF carries out of the option field and the check's (rest - h2) borrows. Its options round trip, it is accepted one step on, refused two on and one before, and data 0 at the same irs round trips. The vectors are printed. 64-bit: no target, and no x86_64 host, has a 64-bit ULONG; ThreadX's linux tx_port.h keeps it 32 bits on x86_64, which is all the fork's 64-bit suite runs. test_syncache_ulong64 builds the same test and nx_tcp_syncache.c with ULONG 64 bits wide (host/ulong64/tx_port.h, with a static assert on the width), as ctest syncache_cookie_ulong64 and syncache_cache_ulong64. The rig's segments carry a 20-byte data offset, not sizeof(NX_TCP_HEADER), which is wider there. tools/ci.sh HOST_TESTS_EXPECTED 551 -> 553 (ctest -N on this tree: 553). Controls, ThreadX 93387b0a, ctl_native2.res (raw rc of every arm): - e5e89f1b: all 14 arms rc 0. - 401cd686 and 6f9097fa (cookie marker reverted / not yet in): cache rc 1 (32-bit and ulong64), 6 failures, all in the inverted expiry block; every other arm rc 0. - 2d44d563: the same 6, nothing else. - 96502647 and ac17df7f: test_syncache does not build (_nx_tcp_syncache_accept and _hold do not exist before 2d44d563); detach keep and v6accept abort (rc 134) on the existing unreached _nx_tcp_socket_state_syn_received stub, because before 2d44d563 the ACK established the socket at once. - The unmodified a5cb72dd test: cache 21 failures at e5e89f1b, 19 at 401cd686. The 2 that differ are the old expiry assertions. Co-Authored-By: Claude Opus 5.5 --- tests/syncache/CMakeLists.txt | 25 + .../syncache/host/test_syncache_detach_host.c | 69 ++- tests/syncache/host/test_syncache_host.c | 449 ++++++++++++++++-- tests/syncache/host/ulong64/tx_port.h | 26 + tools/ci.sh | 5 +- 5 files changed, 515 insertions(+), 59 deletions(-) create mode 100644 tests/syncache/host/ulong64/tx_port.h diff --git a/tests/syncache/CMakeLists.txt b/tests/syncache/CMakeLists.txt index a2ad73860..361a98352 100644 --- a/tests/syncache/CMakeLists.txt +++ b/tests/syncache/CMakeLists.txt @@ -52,6 +52,31 @@ include(CTest) add_test(NAME syncache_cookie COMMAND test_syncache cookie) add_test(NAME syncache_cache COMMAND test_syncache cache) +# The same test with ULONG 64 bits wide. No target this ships for has one, +# and the x86_64 hosts the fork's 64-bit suite runs on keep ULONG at 32 bits +# (ThreadX's linux tx_port.h), so nothing else builds the cookie that way; +# nx_tcp_syncache.c says every step of it masks to 32 bits so that a wide +# ULONG computes the same thing. This is where that is checked: the cookie +# vectors, the counter wrap and the carry, and the cache, on the same file. +add_executable(test_syncache_ulong64 + host/test_syncache_host.c + "${AMINETXDUO_NETXDUO}/common/src/nx_tcp_syncache.c") +target_include_directories(test_syncache_ulong64 PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/host/ulong64" # tx_port.h, first + "${AMINETXDUO_NETXDUO}/ports/linux/gnu/inc" + "${CMAKE_SOURCE_DIR}/port/netxduo-amiga/inc" + "${AMINETXDUO_NETXDUO}/common/inc" + "${AMINETXDUO_THREADX}/common/inc") +target_compile_definitions(test_syncache_ulong64 PRIVATE + NX_PACKET_ALIGNMENT=8 + NX_INCLUDE_USER_DEFINE_FILE + NX_IPV6_UTIL_INLINE + AMINETXDUO_HOST_VENDORED_TX_PORT="${AMINETXDUO_THREADX}/ports/linux/gnu/inc/tx_port.h") +target_compile_options(test_syncache_ulong64 PRIVATE -Wall -Wextra) +set_target_properties(test_syncache_ulong64 PROPERTIES C_STANDARD 11) +add_test(NAME syncache_cookie_ulong64 COMMAND test_syncache_ulong64 cookie) +add_test(NAME syncache_cache_ulong64 COMMAND test_syncache_ulong64 cache) + # Issue #50: a SYN-cache entry outlives the interface its SYN arrived on. # The cache, _nx_ip_interface_detach, the TCP control senders, the IPv4 route # lookup and _nx_ipv6_packet_send are all the shipped files; host_nx_assert.h diff --git a/tests/syncache/host/test_syncache_detach_host.c b/tests/syncache/host/test_syncache_detach_host.c index 3079ceba6..d7ff81480 100644 --- a/tests/syncache/host/test_syncache_detach_host.c +++ b/tests/syncache/host/test_syncache_detach_host.c @@ -155,6 +155,28 @@ UINT _nx_igmp_multicast_interface_leave_internal(NX_IP *ip_ptr, ULONG group, UIN return NX_SUCCESS; } VOID _nx_tcp_socket_connection_reset(NX_TCP_SOCKET *socket_ptr) { (void) socket_ptr; } + +/* nx_tcp_syncache.c calls these two only from _nx_tcp_syncache_accept and + _nx_tcp_syncache_hold (2d44d563), which no arm here calls, and the ACK also + from _nx_tcp_syncache_send_ack (96502647), for a segment carrying data to a + handshake in the accept queue, which no arm sends. A call is a path this + test does not model, so it stops the test rather than being absorbed. + tests/syncache/host/test_syncache_host.c models both and drives them. */ +VOID _nx_tcp_socket_packet_process(NX_TCP_SOCKET *socket_ptr, NX_PACKET *packet_ptr) +{ + (void) socket_ptr; + (void) packet_ptr; + printf("FAIL _nx_tcp_socket_packet_process reached: an unmodelled path\n"); + abort(); +} + +VOID _nx_tcp_packet_send_ack(NX_TCP_SOCKET *socket_ptr, ULONG tx_sequence) +{ + (void) socket_ptr; + (void) tx_sequence; + printf("FAIL _nx_tcp_packet_send_ack reached: an unmodelled path\n"); + abort(); +} #ifdef FEATURE_NX_IPV6 VOID _nx_nd_cache_interface_entries_delete(NX_IP *ip_ptr, UINT index) { @@ -461,11 +483,23 @@ static void case_v6_retry(void) static void case_v6_accept(void) { ULONG iss; + ULONG iss_taker; + /* A SYN with no socket parked is deferred and not answered (96502647), + so the queued handshake comes from two SYNs answered while one socket + is parked: an IPv4 one on K takes the socket, the IPv6 one finishes + with none and queues. */ rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; rig_syn(NX_IP_VERSION_V6, 0x2000); - iss = rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_head -> nx_tcp_syncache_iss; + iss = rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_tail -> nx_tcp_syncache_iss; + rig_syn(NX_IP_VERSION_V4, 0x2100); + iss_taker = rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_tail -> nx_tcp_syncache_iss; + ok("both SYNs are answered", rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_count == 2); + rig_ack(NX_IP_VERSION_V4, 0x2100, iss_taker); + ok("the first finished handshake takes the parked socket", + rig_listen.nx_tcp_listen_socket_ptr == NX_NULL && + rig_socket.nx_tcp_socket_connect_port == 40000); rig_ack(NX_IP_VERSION_V6, 0x2000, iss); ok("the handshake is queued for accept", rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_accept_count == 1); @@ -515,21 +549,40 @@ static void case_keep_other(void) { ULONG iss; + ULONG iss_taker; + + /* Every SYN arrives while a socket is parked, so each is answered + (96502647: with none parked a SYN is deferred, unanswered, and never + retried). The first ACK takes the socket; the second finishes with + none and queues. */ rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; rig_if = 0; rig_syn(NX_IP_VERSION_V4, 0x6000); - iss = rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_head -> nx_tcp_syncache_iss; - rig_ack(NX_IP_VERSION_V4, 0x6000, iss); /* queued on 0 */ - ok("a handshake on interface 0 is queued for accept", - rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_accept_count == 1); + iss = rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_tail -> nx_tcp_syncache_iss; + peer4_other += 2; /* the peer that takes the socket */ + rig_syn(NX_IP_VERSION_V4, 0x6100); + iss_taker = rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_tail -> nx_tcp_syncache_iss; + peer4_other -= 2; peer4_other++; /* another peer on 0 */ rig_syn(NX_IP_VERSION_V4, 0x6200); peer4_other--; rig_if = K; rig_syn(NX_IP_VERSION_V4, 0x6300); /* half-open on K */ + + rig_if = 0; + peer4_other += 2; + rig_ack(NX_IP_VERSION_V4, 0x6100, iss_taker); /* takes the socket */ + peer4_other -= 2; + ok("the first finished handshake takes the parked socket", + rig_listen.nx_tcp_listen_socket_ptr == NX_NULL && + rig_socket.nx_tcp_socket_connect_port == 40000); + rig_ack(NX_IP_VERSION_V4, 0x6000, iss); /* queued on 0 */ + ok("a handshake on interface 0 is queued for accept", + rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_accept_count == 1); + rig_if = K; ok("two half-open, one queued", rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_count == 2 && rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_accept_count == 1); @@ -537,6 +590,8 @@ static void case_keep_other(void) rig_detach(); rig_cache_check(1, 1); ok("the survivors are both on interface 0", + rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_head != NX_NULL && + rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_accept_head != NX_NULL && rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_age_head -> nx_tcp_syncache_interface == &rig_ip.nx_ip_interface[0] && rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_accept_head -> nx_tcp_syncache_interface diff --git a/tests/syncache/host/test_syncache_host.c b/tests/syncache/host/test_syncache_host.c index eba41ceda..5ffc03ae6 100644 --- a/tests/syncache/host/test_syncache_host.c +++ b/tests/syncache/host/test_syncache_host.c @@ -11,6 +11,7 @@ #include "nx_api.h" #include "nx_ip.h" #include "nx_tcp.h" +#include "nx_packet.h" #include #include @@ -115,6 +116,115 @@ VOID _nx_tcp_socket_state_syn_received(NX_TCP_SOCKET *socket_ptr, stub_established++; } +/* What the cache asks of the rest of the stack once a connection is handed + over (2d44d563). Each is a model of what the real one does to what the + cache gives it, checked, so a call the cache should not make, or makes with + the wrong thing, fails the arm instead of being absorbed. */ +static int stub_violations; + +static void stub_violation(const char *what) +{ + printf("FAIL model: %s\n", what); + stub_violations++; +} + +/* _nx_tcp_packet_send_ack: an ACK from a connection that has a peer, at the + sequence number it is handed. Recorded with the window it advertises. */ +static int stub_acks; +static NX_TCP_SOCKET *stub_ack_socket; +static ULONG stub_ack_seq; +static ULONG stub_ack_ack; +static ULONG stub_ack_window; + +VOID _nx_tcp_packet_send_ack(NX_TCP_SOCKET *socket_ptr, ULONG tx_sequence) +{ + if ((socket_ptr == NX_NULL) || (socket_ptr -> nx_tcp_socket_connect_port == 0)) + { + stub_violation("an ACK from a socket with no peer"); + return; + } + stub_acks++; + stub_ack_socket = socket_ptr; + stub_ack_seq = tx_sequence; + stub_ack_ack = socket_ptr -> nx_tcp_socket_rx_sequence; + stub_ack_window = socket_ptr -> nx_tcp_socket_rx_window_current; +} + +/* The rig's packets. A packet is the cache's to hold or release only while + it is allocated: releasing one twice, or one the rig never gave out, is a + violation. */ +#define RIG_PACKETS 4 +static NX_PACKET rig_rx[RIG_PACKETS]; +static UCHAR rig_rx_data[RIG_PACKETS][64]; +static int rig_rx_live[RIG_PACKETS]; +static int stub_released; + +static int rig_rx_index(NX_PACKET *packet_ptr) +{ + int i; + + for (i = 0; i < RIG_PACKETS; i++) + { + if (packet_ptr == &rig_rx[i]) + { + return(i); + } + } + return(-1); +} + +UINT _nx_packet_release(NX_PACKET *packet_ptr) +{ + int i = rig_rx_index(packet_ptr); + + if ((i < 0) || (rig_rx_live[i] == 0)) + { + stub_violation("a packet released that the cache did not own"); + return(NX_PTR_ERROR); + } + rig_rx_live[i] = 0; + stub_released++; + return(NX_SUCCESS); +} + +/* _nx_tcp_socket_packet_process, as an ESTABLISHED socket takes in-order + data: the packet is the socket's, no longer on any queue, and starts at + the next byte the socket expects; the socket takes its data and the + packet. */ +static int stub_processed; +static ULONG stub_processed_bytes; + +VOID _nx_tcp_socket_packet_process(NX_TCP_SOCKET *socket_ptr, NX_PACKET *packet_ptr) +{ + NX_TCP_HEADER *h; + ULONG header_length; + int i = rig_rx_index(packet_ptr); + + if ((i < 0) || (rig_rx_live[i] == 0)) + { + stub_violation("a packet processed that the cache did not own"); + return; + } + if (socket_ptr -> nx_tcp_socket_state != NX_TCP_ESTABLISHED) + { + stub_violation("a held packet processed before the socket is established"); + } + if (packet_ptr -> nx_packet_union_next.nx_packet_tcp_queue_next != (NX_PACKET *) NX_PACKET_ALLOCATED) + { + stub_violation("a held packet processed while still marked queued"); + } + h = (NX_TCP_HEADER *) packet_ptr -> nx_packet_prepend_ptr; + header_length = (h -> nx_tcp_header_word_3 >> NX_TCP_HEADER_SHIFT) << 2; + if (h -> nx_tcp_sequence_number != socket_ptr -> nx_tcp_socket_rx_sequence) + { + stub_violation("a held packet processed out of order"); + } + socket_ptr -> nx_tcp_socket_rx_sequence += packet_ptr -> nx_packet_length - header_length; + stub_processed_bytes += packet_ptr -> nx_packet_length - header_length; + stub_processed++; + rig_rx_live[i] = 0; +} + ULONG _nx_ip_route_find(NX_IP *ip_ptr, ULONG destination_address, NX_INTERFACE **nx_ip_interface, ULONG *next_hop_address) { @@ -210,6 +320,108 @@ static UINT cookie_case(void) _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, count - 1, cookie, &back) == NX_FALSE); + /* The counter at the top of its range. It is read off the clock as a + ULONG and only its low eight bits go into the cookie, so the step after + 0xFFFFFFFF is 0 on a 32-bit ULONG and 0x100000000 on a 64-bit one; both + are the next step. */ + { + ULONG top = 0xFFFFFFFFUL; + ULONG wrap = _nx_tcp_syncache_cookie_build(test_key, tuple, 3, irs, top, 0x2AB); + + ok("minted at counter 0xFFFFFFFF, accepted in its own step", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, top, wrap, &back) == NX_TRUE && + back == 0x2AB); + ok("accepted one step on, at counter 0", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, 0UL, wrap, &back) == NX_TRUE && + back == 0x2AB); + ok("accepted one step on, as top + 1 in ULONG arithmetic", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, top + 1UL, wrap, &back) == NX_TRUE); + ok("refused two steps on, at counter 1", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, 1UL, wrap, &back) == NX_FALSE); + ok("refused two steps on, as top + 2", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, top + 2UL, wrap, &back) == NX_FALSE); + ok("refused the step before, 0xFFFFFFFE", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, 0xFFFFFFFEUL, wrap, &back) == NX_FALSE); + + wrap = _nx_tcp_syncache_cookie_build(test_key, tuple, 3, irs, 0xFFUL, 0x011); + ok("minted at 0xFF, the eight-bit field's top, accepted at 0x100", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, 0x100UL, wrap, &back) == NX_TRUE && + back == 0x011); + ok("and refused at 0x101", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, irs, 0x101UL, wrap, &back) == NX_FALSE); + } + + /* The additive carry. The cookie is h1 + irs + (count << 24) + + ((h2 + data) mod 2^24): pick a peer sequence number for which h1 + irs + overflows 32 bits and h2 + data crosses 2^24, so building it carries + out of the option field and checking it, (rest - h2), borrows. Once + at an ordinary counter and once at 0xFFFFFFFF, where the step after + also wraps. Searched, not assumed: h2 depends on irs and the counter. */ + { + static const ULONG counts[2] = { 12345UL, 0xFFFFFFFFUL }; + UINT k; + + for (k = 0; k < 2; k++) + { + ULONG cnt = counts[k]; + ULONG start; + ULONG c_irs; + ULONG h1; + ULONG h2 = 0; + ULONG counted[5]; + ULONG carry_cookie; + ULONG tries; + int found = 0; + + h1 = _nx_tcp_syncache_hash(&test_key[0], tuple, 3) & 0xFFFFFFFFUL; + start = (0xFFFFFFFFUL - h1 + 1UL) & 0xFFFFFFFFUL; + for (tries = 0, c_irs = start; tries < 0x400000UL; tries++, c_irs = (c_irs + 1UL) & 0xFFFFFFFFUL) + { + counted[0] = tuple[0]; + counted[1] = tuple[1]; + counted[2] = tuple[2]; + counted[3] = cnt & 0xFFFFFFFFUL; + counted[4] = c_irs; + h2 = _nx_tcp_syncache_hash(&test_key[2], counted, 5) & 0xFFFFFFFFUL; + if (((h2 & 0x00FFFFFFUL) > (0x00FFFFFFUL - 0x3FFUL)) && + (((h1 + c_irs) & 0xFFFFFFFFUL) < h1)) + { + found = 1; + break; + } + } + printf(" carry vector at counter %08lx: found %d, h1 %08lx irs %08lx h2 %08lx\n", + (unsigned long) cnt, found, (unsigned long) h1, (unsigned long) c_irs, + (unsigned long) h2); + eq("a carry vector was found within the bound", (unsigned long) found, 1); + if (found == 0) + { + continue; + } + + carry_cookie = _nx_tcp_syncache_cookie_build(test_key, tuple, 3, c_irs, cnt, 0x3FF); + ok("h2 + data crosses 2^24 and h1 + irs 2^32: the options round trip", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, c_irs, cnt, carry_cookie, + &back) == NX_TRUE && back == 0x3FF); + ok("the carry does not move the counter step: accepted one on", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, c_irs, + (cnt + 1UL) & 0xFFFFFFFFUL, carry_cookie, + &back) == NX_TRUE && back == 0x3FF); + ok("refused two on", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, c_irs, + (cnt + 2UL) & 0xFFFFFFFFUL, carry_cookie, + &back) == NX_FALSE); + ok("refused one before", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, c_irs, + (cnt - 1UL) & 0xFFFFFFFFUL, carry_cookie, + &back) == NX_FALSE); + carry_cookie = _nx_tcp_syncache_cookie_build(test_key, tuple, 3, c_irs, cnt, 0x000); + ok("the same irs with data 0, no carry out of the field, round trips", + _nx_tcp_syncache_cookie_check(test_key, tuple, 3, c_irs, cnt, carry_cookie, + &back) == NX_TRUE && back == 0x000); + } + } + /* The peer's own sequence number is bound in, so a cookie cannot be lifted onto a different handshake from the same address. */ ok("refused against a different peer sequence number", @@ -368,6 +580,7 @@ static UINT cookie_case(void) static NX_IP rig_ip; static NX_TCP_LISTEN rig_listen; static NX_TCP_SOCKET rig_socket; +static NX_TCP_SOCKET rig_socket2; /* what a relisten parks next */ static NX_INTERFACE rig_interface; static NX_PACKET rig_packet; @@ -385,7 +598,10 @@ static void rig_reset(void) memset(&rig_ip, 0, sizeof(rig_ip)); memset(&rig_listen, 0, sizeof(rig_listen)); memset(&rig_socket, 0, sizeof(rig_socket)); + memset(&rig_socket2, 0, sizeof(rig_socket2)); memset(&rig_interface, 0, sizeof(rig_interface)); + memset(rig_rx, 0, sizeof(rig_rx)); + memset(rig_rx_live, 0, sizeof(rig_rx_live)); memset(&rig_packet, 0, sizeof(rig_packet)); rig_interface.nx_interface_ip_mtu_size = 1500; @@ -405,6 +621,10 @@ static void rig_reset(void) rig_socket.nx_tcp_socket_state = NX_TCP_LISTEN_STATE; rig_socket.nx_tcp_socket_rx_window_default = 8192; + /* What nx_tcp_server_socket_relisten() hands deliver(): unbound, CLOSED. */ + rig_socket2.nx_tcp_socket_ip_ptr = &rig_ip; + rig_socket2.nx_tcp_socket_state = NX_TCP_CLOSED; + rig_socket2.nx_tcp_socket_rx_window_default = 8192; host_now = 100000; host_ms = 0; @@ -413,6 +633,11 @@ static void rig_reset(void) stub_established = 0; stub_last_socket = NX_NULL; rig_callbacks = 0; + stub_acks = 0; + stub_ack_socket = NX_NULL; + stub_released = 0; + stub_processed = 0; + stub_processed_bytes = 0; _nx_tcp_syncache_initialize(&rig_ip); memcpy(rig_ip.nx_ip_tcp_syncache.nx_tcp_syncache_key, test_key, sizeof(test_key)); @@ -455,6 +680,54 @@ static UINT rig_ack(ULONG n, ULONG irs, ULONG iss) &rig_interface, NX_TRUE, 888); } +/* A segment from the peer of a connection waiting for accept, as + _nx_tcp_packet_process would hand it to _nx_tcp_syncache_hold: header in + host order, `len` bytes of data after it. */ +static NX_PACKET *rig_segment(int slot, ULONG word_3, ULONG seq, ULONG ack, ULONG len) +{ + NX_PACKET *p = &rig_rx[slot]; + NX_TCP_HEADER *h = (NX_TCP_HEADER *) rig_rx_data[slot]; + + memset(p, 0, sizeof(*p)); + memset(rig_rx_data[slot], 0, sizeof(rig_rx_data[slot])); + h -> nx_tcp_header_word_3 = (5UL << NX_TCP_HEADER_SHIFT) | word_3 | 8192UL; + h -> nx_tcp_sequence_number = seq; + h -> nx_tcp_acknowledgment_number = ack; + p -> nx_packet_prepend_ptr = rig_rx_data[slot]; + /* Twenty bytes of header on the wire, the data offset above: not + sizeof(NX_TCP_HEADER), which is wider with a 64-bit ULONG. */ + p -> nx_packet_append_ptr = rig_rx_data[slot] + 20 + len; + p -> nx_packet_length = 20UL + len; + rig_rx_live[slot] = 1; + return(p); +} + +/* A connection the cache handed over and accept has not been called on is + left as upstream left one: bound, in LISTEN, with the peer's port + (2d44d563). */ +static int rig_awaiting_accept(NX_TCP_SOCKET *socket_ptr, ULONG n) +{ + return((socket_ptr -> nx_tcp_socket_state == NX_TCP_LISTEN_STATE) && + (socket_ptr -> nx_tcp_socket_bound_next != NX_NULL) && + (socket_ptr -> nx_tcp_socket_connect_port == (UINT) (30000u + (n & 0xFFFu)))); +} + +/* Fill the cache with answered handshakes while a socket is parked, then take + the socket away: the cache is full and a SYN can only be answered with a + cookie, with no socket on the port (96502647: with no socket a SYN that + finds room is deferred, so the cache is filled while one is parked). */ +static void rig_fill_then_unpark(ULONG base, ULONG irs) +{ + ULONG i; + + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; + for (i = 0; i < NX_TCP_SYNCACHE_SIZE; i++) + { + (void) rig_syn(base + i, irs + i); + } + rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; +} + static UINT cache_case(void) { NX_TCP_SYNCACHE *cache = &rig_ip.nx_ip_tcp_syncache; @@ -480,11 +753,23 @@ static UINT cache_case(void) ok("the ACK is consumed", rig_ack(1, 0x1000, iss_first) == NX_TRUE); eq("the entry is given back", cache -> nx_tcp_syncache_count, 0); - eq("the connection is established", (unsigned long) stub_established, 1); eq("the application is told once", (unsigned long) rig_callbacks, 1); ok("the listen request's socket has been taken", rig_listen.nx_tcp_listen_socket_ptr == NX_NULL); + /* 2d44d563: accept moves it on, not the ACK. */ + eq("the connection waits for accept, not established by the ACK", + (unsigned long) stub_established, 0); + ok("on the parked socket, bound in LISTEN with the peer's port", + rig_awaiting_accept(&rig_socket, 1)); + eq("and nothing is sent while it waits", (unsigned long) stub_acks, 0); + ok("accept connects it", _nx_tcp_syncache_accept(&rig_socket) == NX_TRUE); + eq("the connection is established", (unsigned long) stub_established, 1); ok("and it is the socket that was parked", stub_last_socket == &rig_socket); + eq("accept sends one ACK, which opens the window", (unsigned long) stub_acks, 1); + ok("from that socket, at iss + 1, acknowledging irs + 1", + stub_ack_socket == &rig_socket && stub_ack_seq == iss_first + 1 && stub_ack_ack == 0x1001); + ok("a second accept is not a second connection", + _nx_tcp_syncache_accept(&rig_socket) == NX_FALSE && stub_established == 1); eq("with the sequence numbers the handshake agreed", rig_socket.nx_tcp_socket_tx_sequence, (unsigned long) (iss_first + 1)); eq("and the peer's", rig_socket.nx_tcp_socket_rx_sequence, 0x1001); @@ -504,6 +789,35 @@ static UINT cache_case(void) rig_socket.nx_tcp_socket_ts_recent, 888); #endif + /* Data the peer sends before accept is held on the socket unacknowledged + and processed by accept, in order (2d44d563); a segment that does not + continue it is released. */ + rig_reset(); + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; + { + ULONG iss = rig_syn(2, 0x1100); + + ok("a handshake to hold data for", rig_ack(2, 0x1100, iss) == NX_TRUE && + rig_awaiting_accept(&rig_socket, 2)); + ok("in-order data is held", + _nx_tcp_syncache_hold(&rig_socket, + rig_segment(0, NX_TCP_ACK_BIT | NX_TCP_PSH_BIT, 0x1101, iss + 1, 10)) == NX_TRUE); + ok("a segment that does not follow it is taken", + _nx_tcp_syncache_hold(&rig_socket, + rig_segment(1, NX_TCP_ACK_BIT, 0x1101 + 500, iss + 1, 10)) == NX_TRUE); + eq("and released, not held", (unsigned long) stub_released, 1); + eq("one segment is held", rig_socket.nx_tcp_socket_receive_queue_count, 1); + eq("nothing was acknowledged while held", (unsigned long) stub_acks, 0); + eq("nor processed", (unsigned long) stub_processed, 0); + ok("accept connects it", _nx_tcp_syncache_accept(&rig_socket) == NX_TRUE); + eq("and processes what was held", (unsigned long) stub_processed, 1); + eq("all ten bytes of it", (unsigned long) stub_processed_bytes, 10); + eq("leaving nothing on the hold queue", rig_socket.nx_tcp_socket_receive_queue_count, 0); + eq("after the one window-opening ACK", (unsigned long) stub_acks, 1); + eq("and no packet is left owned by the cache", + (unsigned long) (rig_rx_live[0] + rig_rx_live[1]), 0); + } + rig_reset(); rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; @@ -540,6 +854,9 @@ static UINT cache_case(void) eq("with no entry ever stored for it", cache -> nx_tcp_syncache_count, NX_TCP_SYNCACHE_SIZE); eq("the cookie was recognised", cache -> nx_tcp_syncache_cookies_valid, 1); + ok("the connection waits for accept on the parked socket (2d44d563)", + stub_established == 0 && rig_awaiting_accept(&rig_socket, 9999)); + ok("and accept connects it", _nx_tcp_syncache_accept(&rig_socket) == NX_TRUE); eq("the connection is established", (unsigned long) stub_established, 1); #ifdef NX_ENABLE_TCP_WINDOW_SCALING eq("the window scale survived the cookie", @@ -604,41 +921,74 @@ static UINT cache_case(void) _nx_tcp_syncache_periodic(&rig_ip); eq("the entry is gone", cache -> nx_tcp_syncache_count, 0); - /* The clock has moved less than one cookie counter step, so the ACK is - still inside the window. This is why a cached entry's sequence number - is a cookie too: the client's handshake is not lost with the entry. */ - ok("but the acknowledgment still completes it", - rig_ack(77, 0x6000, iss_first) == NX_TRUE); - eq("from the cookie alone", cache -> nx_tcp_syncache_cookies_valid, 1); - eq("and the connection is made", (unsigned long) stub_established, 1); - - rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; - - for (i = 0; i < 8; i++) + /* The clock has moved less than one cookie counter step, so a cookie + minted with the SYN would still be inside the window. A cached entry's + sequence number is not a cookie (e5e89f1b): the handshake is lost with + the entry, and its acknowledgment goes back to the caller, which answers + an ACK on a port in LISTEN with (RFC 9293 + 3.10.7.2; the fork's netx_3_06, 8_02 and 10_23_01 check that reset). */ + stub_rsts = 0; + stub_synacks = 0; + ok("the acknowledgment of an expired cached entry is not consumed: it is reset", + rig_ack(77, 0x6000, iss_first) == NX_FALSE); + eq("it does not decode as a cookie", cache -> nx_tcp_syncache_cookies_valid, 0); + eq("and is counted as not one", cache -> nx_tcp_syncache_cookies_invalid, 1); + eq("no connection is made", (unsigned long) stub_established, 0); + ok("or rebuilt onto the parked socket", + rig_listen.nx_tcp_listen_socket_ptr == &rig_socket && + rig_socket.nx_tcp_socket_state == NX_TCP_LISTEN_STATE && + rig_socket.nx_tcp_socket_connect_port == 0); + eq("the cache sends nothing for it itself", (unsigned long) (stub_rsts + stub_synacks), 0); + eq("and holds nothing for it", cache -> nx_tcp_syncache_count, 0); + + /* The peer starts again, and that works. */ { - ULONG iss = rig_syn(4000 + i, 0x7000 + i); - - (void) rig_ack(4000 + i, 0x7000 + i, iss); + ULONG iss = rig_syn(77, 0x6100); + + eq("a fresh SYN from the same peer is answered", (unsigned long) stub_synacks, 1); + ok("with a new sequence number", iss != iss_first); + ok("and its ACK completes the handshake", rig_ack(77, 0x6100, iss) == NX_TRUE && + rig_awaiting_accept(&rig_socket, 77)); + ok("which accept connects", _nx_tcp_syncache_accept(&rig_socket) == NX_TRUE && + stub_established == 1); } - eq("a full backlog waits", cache -> nx_tcp_syncache_accept_count, 8); - eq("and none of them reset the peer", (unsigned long) stub_rsts, 0); - eq("and no socket was committed", (unsigned long) stub_established, 0); + /* A SYN with no socket parked is deferred and not answered (96502647), + so a finished handshake with no socket comes from SYNs answered while + one was parked: ten arrive, the first ACK takes the socket, the next + eight queue for accept, and the tenth is past the backlog. */ + rig_reset(); + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; { - ULONG iss = rig_syn(4008, 0x7008); + ULONG iss[10]; - (void) rig_ack(4008, 0x7008, iss); + for (i = 0; i < 10; i++) + { + iss[i] = rig_syn(4000 + i, 0x7000 + i); + } + eq("ten SYNs answered while a socket is parked", (unsigned long) stub_synacks, 10); + ok("the first ACK takes the parked socket", + rig_ack(4000, 0x7000, iss[0]) == NX_TRUE && rig_awaiting_accept(&rig_socket, 4000)); + for (i = 1; i < 9; i++) + { + (void) rig_ack(4000 + i, 0x7000 + i, iss[i]); + } + eq("a full backlog waits", cache -> nx_tcp_syncache_accept_count, 8); + eq("and none of them reset the peer", (unsigned long) stub_rsts, 0); + eq("and no socket was committed", (unsigned long) stub_established, 0); + + (void) rig_ack(4009, 0x7009, iss[9]); } eq("past the backlog the queue does not grow", cache -> nx_tcp_syncache_accept_count, 8); eq("and the peer is told, rather than left hanging", (unsigned long) stub_rsts, 1); - rig_socket.nx_tcp_socket_state = NX_TCP_CLOSED; ok("relisten takes one", _nx_tcp_syncache_deliver(&rig_ip, &rig_listen, - &rig_socket) == NX_TRUE); + &rig_socket2) == NX_TRUE); eq("the queue shortens", cache -> nx_tcp_syncache_accept_count, 7); + ok("onto the relistened socket, waiting for accept", rig_awaiting_accept(&rig_socket2, 4001)); + ok("which accept connects", _nx_tcp_syncache_accept(&rig_socket2) == NX_TRUE); eq("and the connection reaches the application", (unsigned long) stub_established, 1); @@ -650,13 +1000,9 @@ static UINT cache_case(void) eq("and resets every peer waiting on it", (unsigned long) stub_rsts, 7); rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; rig_listen.nx_tcp_listen_rx_window = 65536 * 4; /* needs a scale */ - for (i = 0; i < NX_TCP_SYNCACHE_SIZE; i++) - { - (void) rig_syn(6000 + i, 0xA000 + i); - } + rig_fill_then_unpark(6000, 0xA000); iss_last = rig_syn(6999, 0xB000); /* past full: a cookie */ eq("the cookie SYN-ACK was sent with no socket on the port", cache -> nx_tcp_syncache_cookies_sent, 1); @@ -664,9 +1010,10 @@ static UINT cache_case(void) ok("and it announced a window scale", stub_synack_scale > 0); #endif - rig_socket.nx_tcp_socket_state = NX_TCP_LISTEN_STATE; rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; - ok("the cookie completes", rig_ack(6999, 0xB000, iss_last) == NX_TRUE); + ok("the cookie completes", rig_ack(6999, 0xB000, iss_last) == NX_TRUE && + cache -> nx_tcp_syncache_cookies_valid == 1 && + rig_awaiting_accept(&rig_socket, 6999)); #ifdef NX_ENABLE_TCP_WINDOW_SCALING eq("with the scale the SYN-ACK announced, not the parked socket's", rig_socket.nx_tcp_rcv_win_scale_value, stub_synack_scale); @@ -695,20 +1042,18 @@ static UINT cache_case(void) } rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; rig_listen.nx_tcp_listen_rx_window = 100352; rig_listen.nx_tcp_listen_rx_window_maximum = 262144; - for (i = 0; i < NX_TCP_SYNCACHE_SIZE; i++) - { - (void) rig_syn(7000 + i, 0xA100 + i); - } + rig_socket.nx_tcp_socket_rx_window_default = 100352; + rig_fill_then_unpark(7000, 0xA100); + stub_synack_scale = 0; iss_last = rig_syn(7999, 0xB100); /* past full: a cookie */ + eq("past full with no socket parked, a cookie", cache -> nx_tcp_syncache_cookies_sent, 1); eq("a cookie SYN-ACK announces the grown window's scale too", stub_synack_scale, 3); - rig_socket.nx_tcp_socket_state = NX_TCP_LISTEN_STATE; - rig_socket.nx_tcp_socket_rx_window_default = 100352; rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; - ok("the cookie completes", rig_ack(7999, 0xB100, iss_last) == NX_TRUE); + ok("the cookie completes", rig_ack(7999, 0xB100, iss_last) == NX_TRUE && + cache -> nx_tcp_syncache_cookies_valid == 1); eq("and the ACK reconstructs that scale, not the advertised window's", rig_socket.nx_tcp_rcv_win_scale_value, 3); @@ -721,11 +1066,15 @@ static UINT cache_case(void) eq("no maximum: the scale is the advertised window's", stub_synack_scale, 1); #endif + /* Two SYNs answered while one socket is parked: the first ACK takes the + socket, the second finishes with none (96502647). */ rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; { + ULONG iss87 = rig_syn(87, 0xBF00); ULONG iss = rig_syn(88, 0xC000); + (void) rig_ack(87, 0xBF00, iss87); (void) rig_ack(88, 0xC000, iss); } eq("one finished handshake is waiting", cache -> nx_tcp_syncache_accept_count, 1); @@ -836,36 +1185,32 @@ static UINT cache_case(void) } rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; + rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; host_ms = 3000; { + ULONG iss96 = rig_syn(98, 0xE380); ULONG iss = rig_syn(97, 0xE400); host_ms = 3040; + (void) rig_ack(98, 0xE380, iss96); /* takes the socket */ ok("with no socket parked the handshake is queued", rig_ack(97, 0xE400, iss) == NX_TRUE); eq("in the accept queue", cache -> nx_tcp_syncache_accept_count, 1); host_ms = 9000; - rig_socket.nx_tcp_socket_state = NX_TCP_LISTEN_STATE; - rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; ok("a relisten takes it", _nx_tcp_syncache_deliver(&rig_ip, &rig_listen, - &rig_socket) == NX_TRUE); + &rig_socket2) == NX_TRUE); eq("with the round trip of the handshake, not of the wait", - rig_socket.nx_tcp_socket_handshake_rtt, 40); + rig_socket2.nx_tcp_socket_handshake_rtt, 40); } rig_reset(); - rig_listen.nx_tcp_listen_socket_ptr = NX_NULL; host_ms = 4000; - for (i = 0; i < NX_TCP_SYNCACHE_SIZE; i++) - { - (void) rig_syn(8000 + i, 0xF000 + i); - } + rig_fill_then_unpark(8000, 0xF000); iss_last = rig_syn(8999, 0xF999); /* past full: a cookie */ host_ms = 4030; - rig_socket.nx_tcp_socket_state = NX_TCP_LISTEN_STATE; rig_listen.nx_tcp_listen_socket_ptr = &rig_socket; - ok("a cookie completes", rig_ack(8999, 0xF999, iss_last) == NX_TRUE); + ok("a cookie completes", rig_ack(8999, 0xF999, iss_last) == NX_TRUE && + cache -> nx_tcp_syncache_cookies_valid == 1); eq("and carries no round trip: nothing was stored to time it", rig_socket.nx_tcp_socket_handshake_rtt, 0); @@ -891,6 +1236,8 @@ int main(int argc, char **argv) return 2; } + failures += stub_violations; + if (failures != 0) { printf("syncache %s: %d failures\n", which, failures); diff --git a/tests/syncache/host/ulong64/tx_port.h b/tests/syncache/host/ulong64/tx_port.h new file mode 100644 index 000000000..a2583ecfb --- /dev/null +++ b/tests/syncache/host/ulong64/tx_port.h @@ -0,0 +1,26 @@ +/* + * AmiNetXDuo, host shim for tests/syncache: ThreadX's port header with ULONG + * 64 bits wide, the one width tests/perf/host/shim does not give. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef AMINETXDUO_HOST_TX_PORT_ULONG64 +#define AMINETXDUO_HOST_TX_PORT_ULONG64 + +#ifndef AMINETXDUO_HOST_VENDORED_TX_PORT +#error "AMINETXDUO_HOST_VENDORED_TX_PORT must name the tx_port.h to wrap" +#endif + +#define LONG aminetxduo_host_ulong64_LONG +#define ULONG aminetxduo_host_ulong64_ULONG +#include AMINETXDUO_HOST_VENDORED_TX_PORT +#undef LONG +#undef ULONG + +typedef long LONG; +typedef unsigned long ULONG; + +_Static_assert(sizeof(ULONG) == 8, "this shim is for a 64-bit ULONG"); + +#endif /* AMINETXDUO_HOST_TX_PORT_ULONG64 */ diff --git a/tools/ci.sh b/tools/ci.sh index 6e987ff61..893f65a28 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -470,7 +470,10 @@ host_test_targets() { # builddir # 551 with tls_send_record_ownership (GHSA-8w5x-ff58-2fr2): a TLS # record's packet chain is left alone once TCP has accepted it, and # wiped only on a failed send (all hosts). -HOST_TESTS_EXPECTED=551 +# 553 with syncache_cookie_ulong64/_cache_ulong64: the SYN cache's +# cookie, counter wrap, carry and cache with ULONG 64 bits wide +# (all hosts). +HOST_TESTS_EXPECTED=553 case "$(uname -m)" in x86_64|amd64) ;; # test_inet, test_route, test_expunge, test_expunge_cork, test_select, From 2509ed298c5d446fe3edf06dd93d74e8a5ce827f Mon Sep 17 00:00:00 2001 From: turo Date: Sat, 3 Oct 2026 18:23:05 -0700 Subject: [PATCH 3/7] tests/syncache: detach's _nx_packet_release fails fast too Test-only follow-up to 87305b74 (review note). test_syncache_detach's _nx_packet_release was still a no-op beside the two fail-fast stubs. Counted in all ten arms (v4 v4reuse keep gap driver late v6 v6accept v6delete v6late): 0 calls in each. The IPv4 send and _nx_ipv6_header_add stubs keep their packets, and the cache's own releases are in _nx_tcp_syncache_hold, which no arm reaches. So a call is now an unmodelled path and aborts the test, as _nx_tcp_socket_packet_process and _nx_tcp_packet_send_ack do. All ten arms rc 0 after the change (netxduo e5e89f1b, ThreadX e24aa9c9). Co-Authored-By: Claude Opus 5.5 --- tests/syncache/host/test_syncache_detach_host.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/syncache/host/test_syncache_detach_host.c b/tests/syncache/host/test_syncache_detach_host.c index d7ff81480..cae4fe55b 100644 --- a/tests/syncache/host/test_syncache_detach_host.c +++ b/tests/syncache/host/test_syncache_detach_host.c @@ -128,7 +128,16 @@ UINT _nx_packet_allocate(NX_PACKET_POOL *pool_ptr, NX_PACKET **packet_ptr, return NX_SUCCESS; } -UINT _nx_packet_release(NX_PACKET *packet_ptr) { (void) packet_ptr; return NX_SUCCESS; } +/* No arm releases a packet (counted: 0 calls in all ten): the IPv4 send and + _nx_ipv6_header_add above keep theirs, and the cache's own releases are in + _nx_tcp_syncache_hold, which no arm reaches. A call is a path this test + does not model, so it stops the test, as the two below do. */ +UINT _nx_packet_release(NX_PACKET *packet_ptr) +{ + (void) packet_ptr; + printf("FAIL _nx_packet_release reached: an unmodelled path\n"); + abort(); +} UINT _nx_packet_transmit_release(NX_PACKET *packet_ptr) { (void) packet_ptr; return NX_SUCCESS; } /* A SYN fed into the cache from inside the detach: in the unlocked ARP/ND From 6875b54024fcbff704da992b136cf65b8891cbbb Mon Sep 17 00:00:00 2001 From: turo Date: Sat, 3 Oct 2026 18:52:04 -0700 Subject: [PATCH 4/7] tests/syncache: the expiry block says what it proves, not the RST Test-only. Corrects the wording of 87305b74. Its message ("the caller resets it"), the assertion label at test_syncache_host.c:932 ("...is not consumed: it is reset") and the comment above it implied that this native test forms or checks the RST for an ACK to an expired cached entry. It does neither. The rig does not link _nx_tcp_packet_process. What the block proves: _nx_tcp_syncache_ack_received returns NX_FALSE (the ACK is not consumed) and the cache sends nothing for it. This native test is not proof of the RST. The on-the-wire RST is checked by the fork's netx_3_06, netx_8_02 and netx_10_23_01 and by ctl_syncookie. The label and the comment change; no assertion, value or code changes. test_syncache cache rc 0, test_syncache_ulong64 cache rc 0 (ThreadX e24aa9c9, netxduo e5e89f1b). Co-Authored-By: Claude Opus 5.5 --- tests/syncache/host/test_syncache_host.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/syncache/host/test_syncache_host.c b/tests/syncache/host/test_syncache_host.c index 5ffc03ae6..fcffb2394 100644 --- a/tests/syncache/host/test_syncache_host.c +++ b/tests/syncache/host/test_syncache_host.c @@ -924,12 +924,14 @@ static UINT cache_case(void) /* The clock has moved less than one cookie counter step, so a cookie minted with the SYN would still be inside the window. A cached entry's sequence number is not a cookie (e5e89f1b): the handshake is lost with - the entry, and its acknowledgment goes back to the caller, which answers - an ACK on a port in LISTEN with (RFC 9293 - 3.10.7.2; the fork's netx_3_06, 8_02 and 10_23_01 check that reset). */ + the entry. What this rig proves is the cache's side only: the ACK is + not consumed (NX_FALSE) and the cache sends nothing for it. It does + not link _nx_tcp_packet_process and is no evidence of the RST that + goes on the wire; the fork's netx_3_06, 8_02 and 10_23_01 and + ctl_syncookie check that. */ stub_rsts = 0; stub_synacks = 0; - ok("the acknowledgment of an expired cached entry is not consumed: it is reset", + ok("the acknowledgment of an expired cached entry is not consumed (NX_FALSE)", rig_ack(77, 0x6000, iss_first) == NX_FALSE); eq("it does not decode as a cookie", cache -> nx_tcp_syncache_cookies_valid, 0); eq("and is counted as not one", cache -> nx_tcp_syncache_cookies_invalid, 1); From 23e42b02f2bc4dbd71b5bbb908fa0cc1c11fa694 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Sun, 4 Oct 2026 03:31:35 -0700 Subject: [PATCH 5/7] tests/netstack: test_tcp_sws expects the fork's silly-window rules Two fork commits in the 6.5.2 pin changed what this test asserted, and it failed 8 of its 43 checks (the seq31 and seq32 arms the same): - a820b430 "tcp: the sender's silly-window test sends a pushed write that fits": RFC 1122 4.2.3.4 rule (2) with Nagle off. A pushed write that fits the usable window whole (D <= U) goes now. - 5615cfd8 "tcp: below the SWS floor, the receiver holds its right edge instead of advertising zero": NX_TCP_RX_WINDOW_ADVERTISED (nx_tcp.h). At or above the floor, min(MSS, buffer / 2), the free space goes out. Below it, the edge last sent is held, or the free space if that is less. The 8 expectations, old -> new: 1. b, 100-byte write, 200 usable, 512 in flight: 0 datagrams -> 1. 2. b, the same write: NX_WINDOW_OVERFLOW -> NX_SUCCESS. 3. e, 1200-byte write, 1200 usable, 65535 peak: 0 datagrams -> 1. 4. e, the same write: NX_WINDOW_OVERFLOW -> NX_SUCCESS. 5. k, 68 free, 68 last sent: advertised 0 -> 68. 6. k, 104 free, 104 last sent: 0 -> 104. 7. k, MSS - 1 free and last sent: 0 -> MSS - 1. 8. m, 100 free and last sent, 1000-byte buffer: 0 -> 100. The names say the new rules: b_sliver_with_flight_holds becomes b_sliver_with_flight_sends_a_write_that_fits, and k_a_runt_window_is_advertised_as_zero becomes k_a_runt_window_already_offered_is_kept. Their messages change to match. The negative controls stay, and some are added: - b and e: a write larger than the usable window is still held whole, with NX_WINDOW_OVERFLOW and no datagram (300 into 200, 1300 into 1200), and the persist probe is not armed. - i keeps the H_MSS waiter: a 200-byte sliver wakes no one, and a full segment wakes it once. - h_advertise_edge() sets the edge last sent apart from the free space, and asserts that rx_window_last_sent is the window that went on the wire. - o_below_the_floor_the_edge_holds: - 1000 free with 200 last sent holds 200; - 100 free with a zero edge stays 0, and so does MSS - 1; - MSS free reopens a zero edge to MSS; - 100 free with 300 last sent is a genuine shrink, and goes out as 100. - m: a 1000-byte buffer keeps 100 already offered, holds a zero edge at 100 free, and reopens it at its floor, 500. - l and n are unchanged. The test now runs 78 checks, all passing, in each of tcp_sws, tcp_sws_seq31 and tcp_sws_seq32. No shipping or vendored source changes. Co-Authored-By: Claude Opus 5.5 --- tests/netstack/host/test_tcp_sws_host.c | 146 ++++++++++++++++++++---- 1 file changed, 121 insertions(+), 25 deletions(-) diff --git a/tests/netstack/host/test_tcp_sws_host.c b/tests/netstack/host/test_tcp_sws_host.c index 67cc85f1b..0acffd03f 100644 --- a/tests/netstack/host/test_tcp_sws_host.c +++ b/tests/netstack/host/test_tcp_sws_host.c @@ -372,7 +372,10 @@ static void a_open_window_sends(void) (unsigned long)h_sock.nx_tcp_socket_tx_window_advertised); } -static void b_sliver_with_flight_holds(void) +/* RFC 1122 4.2.3.4 rule (2) with Nagle off, as it is here: a pushed write + that fits whole in the usable window (D <= U) goes now, even into a sliver + with data in flight (a820b430). One that does not fit is still held. */ +static void b_sliver_with_flight_sends_a_write_that_fits(void) { UINT status; @@ -381,16 +384,34 @@ static void b_sliver_with_flight_holds(void) status = h_write(100); + h_check_eq(status, NX_SUCCESS, + "a 100-byte write that fits 200 bytes of usable window was " + "refused (rule 2: a pushed write that fits goes now)"); + h_check_eq(h_datagrams, 1, + "a 100-byte write that fits 200 bytes of usable window with " + "512 bytes in flight was withheld (rule 2)"); + + h_check(h_sock.nx_tcp_socket_zero_window_probe_has_data == NX_FALSE, + "a non-zero window armed the zero-window persist probe"); + + printf(" sliver, in flight fits: %u datagram(s), status %u\n", + (unsigned int)h_datagrams, (unsigned int)status); + + /* The negative control: a write larger than the sliver is held, whole. */ + h_fixture(); + h_in_flight(512, 200); + + status = h_write(300); + h_check_eq(h_datagrams, 0, - "a 100-byte write went out into 200 bytes of usable window " + "a 300-byte write went out into 200 bytes of usable window " "with 512 bytes still in flight (no sender SWS avoidance)"); h_check_eq(status, NX_WINDOW_OVERFLOW, "a send the window rule refused did not report the window"); - h_check(h_sock.nx_tcp_socket_zero_window_probe_has_data == NX_FALSE, - "a non-zero window armed the zero-window persist probe"); + "a held write into a non-zero window armed the persist probe"); - printf(" sliver, in flight %u datagram(s), status %u\n", + printf(" sliver, in flight too big: %u datagram(s), status %u\n", (unsigned int)h_datagrams, (unsigned int)status); } @@ -447,21 +468,36 @@ static void e_half_the_max_window_releases(void) h_check_eq(h_datagrams, 1, "1200 usable bytes of a 2400-byte peer window sent nothing"); - /* The same 1200 bytes, on a peer that has offered 64 KB before now: this - one IS dribbling, and 1200 is neither a full segment nor half of what it - has shown it can take. */ + /* The same 1200 usable bytes on a peer that has offered 64 KB before now: + 1200 is neither a full segment nor half of what it has shown it can + take, so rules (1) and (3) do not release it. A write of exactly 1200 + still goes, by rule (2): it fits whole (a820b430). */ h_fixture(); h_in_flight(512, 1200); status = h_write(1200); + h_check_eq(status, NX_SUCCESS, + "a 1200-byte write that fits 1200 usable bytes of a 65535-byte " + "peer window was refused (rule 2)"); + h_check_eq(h_datagrams, 1, + "a 1200-byte write that fits 1200 usable bytes of a 65535-byte " + "peer window sent nothing (rule 2)"); + + /* One that does not fit is held: no rule releases it. */ + h_fixture(); + h_in_flight(512, 1200); + + status = h_write(1300); + h_check_eq(h_datagrams, 0, - "1200 usable bytes of a 65535-byte peer window went out"); + "a 1300-byte write into 1200 usable bytes of a 65535-byte " + "peer window went out"); h_check_eq(status, NX_WINDOW_OVERFLOW, "a send the window rule refused did not report the window"); - printf(" 1200 usable sent under a 2400-byte peak, held under " - "65535\n"); + printf(" 1200 usable sent under a 2400-byte peak; under 65535 " + "a write that fits goes, a bigger one is held\n"); } static void f_zero_window_still_persists(void) @@ -572,16 +608,19 @@ static void j_peak_window_is_remembered(void) (unsigned long)h_sock.nx_tcp_socket_tx_window_advertised_max); } -/* One pure ACK out of the real control path, with rx_window_current at - `current` and the socket's buffer at `dflt`. Answers the window that - reached the wire. */ -static ULONG h_advertise(ULONG current, ULONG dflt) +/* One pure ACK out of the real control path, with rx_window_current (the + free space) at `current`, the edge last put on the wire at `last_sent`, + and the socket's buffer at `dflt`. Answers the window that reached the + wire. NX_TCP_RX_WINDOW_ADVERTISED (nx_tcp.h, 5615cfd8): at or above the + floor, min(MSS, buffer / 2), the free space; below it, the edge last sent + is held, or the free space if that is less. */ +static ULONG h_advertise_edge(ULONG current, ULONG last_sent, ULONG dflt) { h_fixture(); h_sock.nx_tcp_socket_rx_window_default = dflt; h_sock.nx_tcp_socket_rx_window_current = current; - h_sock.nx_tcp_socket_rx_window_last_sent = current; + h_sock.nx_tcp_socket_rx_window_last_sent = last_sent; h_last_window = 0xFFFFFFFFUL; h_acks = 0; @@ -595,23 +634,68 @@ static ULONG h_advertise(ULONG current, ULONG dflt) h_alloc_ok = 0; h_check(h_acks == 1, "the control path sent no acknowledgment"); + h_check_eq(h_sock.nx_tcp_socket_rx_window_last_sent, h_last_window, + "rx_window_last_sent is not the window that went on the wire"); return h_last_window; } -static void k_a_runt_window_is_advertised_as_zero(void) +/* The edge last sent equal to the free space: what an established socket + has after an acknowledgment that drained nothing. */ +static ULONG h_advertise(ULONG current, ULONG dflt) +{ + return h_advertise_edge(current, current, dflt); +} + +/* Below the floor, a window already offered is not withdrawn (RFC 9293 + 3.8.6.2.2): with the edge last sent equal to the free space, the free space + goes out again rather than zero (5615cfd8). */ +static void k_a_runt_window_already_offered_is_kept(void) { ULONG w; w = h_advertise(68UL, 8192UL); - h_check(w == 0UL, "68 bytes of window was advertised rather than zero"); + h_check_eq(w, 68UL, "68 bytes of window already offered was not kept"); w = h_advertise(104UL, 8192UL); - h_check(w == 0UL, "104 bytes of window was advertised rather than zero"); + h_check_eq(w, 104UL, "104 bytes of window already offered was not kept"); w = h_advertise(H_MSS - 1UL, 8192UL); - h_check(w == 0UL, "one byte short of an MSS was advertised rather than " - "zero"); + h_check_eq(w, H_MSS - 1UL, "one byte short of an MSS, already offered, " + "was not kept"); +} + +/* Below the floor, the edge is not moved (RFC 1122 4.2.3.3): free space past + the edge last sent opens no new sliver, and a zero edge stays zero. Less + free space than the edge is a genuine shrink, and goes out as it is. */ +static void o_below_the_floor_the_edge_holds(void) +{ +ULONG w; + + /* 1000 free, 200 on the wire, floor 1460 (8192-byte buffer). */ + w = h_advertise_edge(1000UL, 200UL, 8192UL); + h_check_eq(w, 200UL, "below the floor, free space past the edge opened " + "a new sliver"); + + w = h_advertise_edge(100UL, 0UL, 8192UL); + h_check_eq(w, 0UL, "below the floor, a zero edge was reopened by a " + "runt of free space"); + + w = h_advertise_edge(H_MSS - 1UL, 0UL, 8192UL); + h_check_eq(w, 0UL, "one byte short of the floor reopened a zero edge"); + + /* At the floor the free space goes out, whatever the edge was. */ + w = h_advertise_edge(H_MSS, 0UL, 8192UL); + h_check_eq(w, H_MSS, "free space at the floor did not reopen a zero " + "edge"); + + /* A genuine shrink: the buffer itself has less than the edge promised. */ + w = h_advertise_edge(100UL, 300UL, 8192UL); + h_check_eq(w, 100UL, "less free space than the edge was not advertised " + "as it is"); + + printf(" held edge kept below the floor, reopened at it, " + "shrunk with the buffer\n"); } /* And the rule stops exactly at one MSS: a window a sender can fill is never @@ -639,9 +723,20 @@ ULONG w; h_check(w == 600UL, "a small-buffer socket advertised zero and could " "never reopen"); - /* Below half of it, the rule still applies. */ + /* Below half of it, 100 already offered is kept (5615cfd8), and a zero + edge is not reopened by it. */ w = h_advertise(100UL, 1000UL); - h_check(w == 0UL, "a runt below half a small buffer was still advertised"); + h_check_eq(w, 100UL, "a runt below half a small buffer, already offered, " + "was not kept"); + + w = h_advertise_edge(100UL, 0UL, 1000UL); + h_check_eq(w, 0UL, "a runt below half a small buffer reopened a zero " + "edge"); + + /* At half of it, 500, the small buffer reopens a zero edge. */ + w = h_advertise_edge(500UL, 0UL, 1000UL); + h_check_eq(w, 500UL, "a small buffer at its floor did not reopen a zero " + "edge"); } /* A window that is genuinely zero is still zero, and is not confused with one @@ -664,7 +759,7 @@ int main(void) (unsigned long)(H_PEER_WINDOW >> 1)); a_open_window_sends(); - b_sliver_with_flight_holds(); + b_sliver_with_flight_sends_a_write_that_fits(); c_small_write_is_not_delayed(); d_nothing_in_flight_always_sends(); e_half_the_max_window_releases(); @@ -675,10 +770,11 @@ int main(void) printf("RFC 1122 4.2.3.3 receiver silly-window avoidance, against the real " "control path\n"); - k_a_runt_window_is_advertised_as_zero(); + k_a_runt_window_already_offered_is_kept(); l_a_full_segment_is_advertised(); m_a_buffer_below_one_mss_still_opens(); n_zero_stays_zero(); + o_below_the_floor_the_edge_holds(); printf("%lu checks, %lu failures, %s\n", h_checks, h_failures, (h_failures == 0UL) ? "PASS" : "FAIL"); From 837644b4f6fb5d44df3387ee56be8aa55b25b2d6 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Sun, 4 Oct 2026 04:30:45 -0700 Subject: [PATCH 6/7] tests/x509: test_pss_schemes gives its bare session the CertificateVerify scratch 20046b12 makes the CertificateVerify signature buffers per session: nx_secure_tls_session_create_ext() carves NX_SECURE_TLS_CERTIFICATE_ VERIFY_SCRATCH_SIZE bytes from the crypto metadata area and points nx_secure_tls_certificate_verify_scratch at it (with its _size), and _nx_secure_tls_process_certificate_verify() addresses its buffers from that pointer. test_pss_schemes() zeroes a bare session and calls the processing directly, so the pointer was NULL: UBSan in the macOS host tier (run 37198121405, job 111424120344) stopped at nx_secure_tls_process_certificate_verify.c:169, "applying non-zero offset 164 to null pointer"; clang 18 on Linux stops at :168. The test now gives that session the area itself: a ULONG array of NX_SECURE_TLS_CERTIFICATE_VERIFY_SCRATCH_SIZE bytes in the same scope as the session, attached as the carve attaches it, after each of the two memsets that reset the session. The four rejection checks are unchanged, and so are the sanitizers. No library change. Co-Authored-By: Claude Opus 5.5 --- tests/x509/test_tls_x509.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/x509/test_tls_x509.c b/tests/x509/test_tls_x509.c index 8647ee24a..d7eb5cc62 100644 --- a/tests/x509/test_tls_x509.c +++ b/tests/x509/test_tls_x509.c @@ -766,9 +766,23 @@ UINT params_offset; } } +/* nx_secure_tls_session_create_ext() carves a session's CertificateVerify + scratch out of the crypto metadata area and points the session at it. + test_pss_schemes() zeroes a bare session instead, so it gives the session + the same area itself, ULONG-aligned as the carve is, after each reset. */ +static void attach_certificate_verify_scratch(NX_SECURE_TLS_SESSION *session, + ULONG *scratch) +{ + session -> nx_secure_tls_certificate_verify_scratch = scratch; + session -> nx_secure_tls_certificate_verify_scratch_size = + NX_SECURE_TLS_CERTIFICATE_VERIFY_SCRATCH_SIZE; +} + static void test_pss_schemes(void) { NX_SECURE_TLS_SESSION session; +ULONG cv_scratch[(NX_SECURE_TLS_CERTIFICATE_VERIFY_SCRATCH_SIZE + + sizeof(ULONG) - 1) / sizeof(ULONG)]; NX_SECURE_X509_CRYPTO method; NX_SECURE_X509_CERT certificate; UCHAR verify_message[4]; @@ -780,6 +794,7 @@ UINT status; printf("pss schemes\n"); memset(&session, 0, sizeof(session)); + attach_certificate_verify_scratch(&session, cv_scratch); memset(&method, 0, sizeof(method)); session.nx_secure_tls_1_3 = 1; method.nx_secure_x509_public_cipher_method = &crypto_method_rsa; @@ -800,6 +815,7 @@ UINT status; "SHA-512 advertises both PSS key encodings"); memset(&session, 0, sizeof(session)); + attach_certificate_verify_scratch(&session, cv_scratch); memset(&certificate, 0, sizeof(certificate)); status = _nx_secure_x509_certificate_initialize(&certificate, (UCHAR *)x509_psskey_root, From 4994af150ce71a31788ca0e7d5edbd573d067f64 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Sun, 4 Oct 2026 08:59:48 -0700 Subject: [PATCH 7/7] tools: budgets for NetX Duo 6.5.2, the fork's merge of upstream 20046b12 The upstream v6.5.2.202603_rel merge (20046b12), pinned in this branch, moves four measured numbers past their gates. turo decided to raise them to the values measured in CI run 37205467139: the Linux job 111445651664 (default), cross micro 111445652093 and cross minimal 111445652048. tools/check-hotpath-budget.sh, the default arm's instruction count: - __nx_tcp_socket_state_data_check: 483 -> 493 tools/check-ram-size.sh, sizeof(AmiNetStack): - default: 68608 -> 68836 - minimal: 17408 -> 17928 - micro: 16384 -> 16388 The TLS session's growth (+8 bytes) and the per-session CertificateVerify scratch are per-connection heap from the TLS metadata area, not part of sizeof(AmiNetStack). The AmiNetStack growth comes from the NetX Duo structures and configuration that the merge changed. It is not attributed further here; tracking its source is deferred. Only these four numbers change. No other arm, gate, option or source file changes. Co-Authored-By: Claude Opus 5.5 --- tools/check-hotpath-budget.sh | 2 +- tools/check-ram-size.sh | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/check-hotpath-budget.sh b/tools/check-hotpath-budget.sh index e4680e3b9..c560e693a 100755 --- a/tools/check-hotpath-budget.sh +++ b/tools/check-hotpath-budget.sh @@ -130,7 +130,7 @@ TABLE = { # on the data-ACK ramp's limit, so a 256 KB receive window (the WAN case # of bsdsocket_window.h) does not acknowledge every 128 KB. Four # instructions per segment, bought on purpose. - "nx_tcp_socket_state_data_check.c": [("__nx_tcp_socket_state_data_check", 483)], # 429; 483 counts + "nx_tcp_socket_state_data_check.c": [("__nx_tcp_socket_state_data_check", 493)], # 429; 483 counts # the queue-cap drop # in the current default # arm (off the in-order path) diff --git a/tools/check-ram-size.sh b/tools/check-ram-size.sh index d630cd0d8..7eb27c2a0 100755 --- a/tools/check-ram-size.sh +++ b/tools/check-ram-size.sh @@ -58,11 +58,11 @@ ARM="${AMINETXDUO_RAM_ARM:-$(basename "$BUILD")}" # the same listener and pointer costs, with no mDNS. DHCP/DNS packet storage # remains dynamic and is therefore not hidden in this resident allocation. BUDGETS=( - "default:68608" - "minimal:17408" + "default:68836" + "minimal:17928" # First budgeted as a shipping profile at 0.28.9: 15,716 bytes, with # headroom to the next KiB boundary. - "micro:16384" + "micro:16388" ) budget=""