Files
dotfiles/overlays/patches/gnutls/README.md
T
Richie 5a59c4d0f5 fix(gnutls): replace UDP test skip with readiness polling
Wait for the UDP socket to bind before starting the client, detect
server exit, and clean up on skip. Preserve both handshake checks.

Add regression checks and document the rationale for upstream submission.

Validated on x86-64-v3: 796 passes, 131 existing skips, zero failures.
All seven focused checks and the delayed-start reproduction passed.
2026-09-18 12:08:10 -04:00

8.6 KiB

GnuTLS UDP server readiness

Under load, the test client can start before gnutls-serv binds its UDP socket, and the first handshake fails with Connection refused. serv-udp.sh currently waits a fixed four seconds; elapsed time does not establish server readiness. udp-server-readiness.patch replaces that wait with polling for the local IPv4 UDP endpoint.

Scope and waiting behavior

The patch changes the existing wait_udp_server() and adds a new check_if_udp_port_bound() beside it in tests/scripts/common.sh. serv-udp.sh is its only caller in 3.8.13. The TCP helpers wait_server() and wait_for_port(), including their existing sleeps, are unchanged. Both original DTLS handshake checks remain unchanged.

Each iteration checks process liveness and the socket before sleeping. A ready socket returns immediately. An unsuccessful check sleeps two seconds only if another attempt remains: at most 90 attempts, consistent with the existing wait_server() budget implemented by wait_for_port(), with no sleep after the final check. Server exit fails early; exhausting the budget fails and terminates the server. No handshake is retried, and no protocol timeout is changed. Once bound, the kernel can queue datagrams while the server is scheduled; the probe itself sends no packets.

The existing have_port_finder() prefers ss, then netstat. If neither exists, it prints neither ss nor netstat found and exits 77 (skip). In the normal test flow, port selection calls it before launching a server. The probe runs in a subshell so that, even if this skip occurs after launch, the waiting helper can terminate and reap the server before exiting 77.

Why an IPv4 socket is expected

This is specific to the server used by this test, not a general rule that IPv6 sockets cannot serve IPv4 clients. The client explicitly uses 127.0.0.1. The server's --udp path calls udp_server(), which calls listen_socket(..., SOCK_DGRAM). That function iterates the wildcard addresses returned by getaddrinfo(NULL, ..., AI_PASSIVE):

Server build / Linux setting Binding behavior
IPv6 enabled, net.ipv6.bindv6only=0 Requests IPV6_V6ONLY=1 on the IPv6 socket, binds [::]:PORT, and separately binds 0.0.0.0:PORT. It overrides the system's dual-stack default.
IPv6 enabled, net.ipv6.bindv6only=1 The same explicit socket option and separate IPv4/IPv6 binds.
HAVE_IPV6 undefined Skips every address family except AF_INET; only the IPv4 wildcard is attempted.

udp_server() uses wait_for_connection(), which puts every listener from that list into select() and returns a readable socket for recvfrom(); it does not permanently choose one socket based on getaddrinfo() order.

The first two cases were traced with the actual GnuTLS 3.8.13 binary in separate Linux network namespaces: setsockopt(IPV6_V6ONLY, [1]) and both UDP binds returned success under each setting. The no-IPv6 case was checked in source, not by building a second binary. The same bind implementation was checked directly on GitLab master.

Thus, successful normal startup for this invocation provides an explicit IPv4 socket; a lone IPv6 wildcard is not the expected success path. There is one portability caveat: upstream discards the return value of setsockopt(IPV6_V6ONLY). On a platform where that call fails and the server ends up with only a dual-stack socket, this helper would time out despite IPv4 reachability. Such a platform needs additional handling before this patch can claim support. Blindly accepting every IPv6 wildcard would also accept IPv6-only sockets before the separate IPv4 bind finishes.

Source: src/serv.c, listen_socket(), src/udp-serv.c, and tests/serv-udp.sh.

Port matching and ownership limit

Only -an is passed to the socket-listing tool: BSD netstat -u selects Unix-domain sockets, whereas Linux netstat -u selects UDP. The parser handles the extra state column in ss, Linux colon-separated endpoints, and BSD dot-separated endpoints, including *.PORT. It matches the full local port and rejects TCP, IPv6 entries, peer ports, and longer numbers.

A live PID plus a bound port does not prove that PID owns the socket. Existing GETPORT selection checks for an unused port and uses a test port-lock directory; launch_bare_server() also calls wait_for_free_port() before starting the process. These are advisory: the launcher does not enforce the latter's result, and another process can bind between the check and launch. The patch does not close that race or add nonportable PID parsing. An unrelated process can satisfy the socket check; the real handshakes remain the functional check and may fail (or reach the wrong server). This is a startup-order fix, not a socket-ownership guarantee.

Reproduction and focused checks

Apply the patch to an unpacked source tree, then run the companion checks with Python's standard library and a shell:

patch --fuzz=0 -d /path/to/gnutls -p1 < udp-server-readiness.patch
SHELL=/bin/sh python3 verify-readiness.py /path/to/gnutls/tests/scripts/common.sh -v

Set NETSTAT=/path/to/netstat to exercise one outside PATH. The checks cover Linux/BSD output samples, false matches, immediate readiness, missing tools, process exit, timeout cleanup, and real IPv4 UDP sockets whose bind is delayed six seconds. The missing-tools fixture is skipped if an absolute fallback ss path cannot be hidden with PATH. Native BSD execution remains untested.

To reproduce with GnuTLS itself, run tests/serv-udp.sh with SERV pointing to a wrapper that sleeps six seconds, then execs gnutls-serv with all arguments. Set CLI to the matching gnutls-cli, srcdir to the source tests directory, and abs_top_builddir to a writable build directory. With GnuTLS 3.8.13, the original helper failed the first handshake with Connection refused; the patched helper passed both with the same binaries.

GnuTLS submission

Development and merge requests are on GitLab. CONTRIBUTING.md on master was read directly for this review. It requires the contributor's DCO Signed-off-by, successful and failure test coverage, consistent coding style, and adequate documentation; GitLab CI runs for merge requests. Its commenting guidance asks for comments explaining non-obvious behavior or protocol expectations. It does not prescribe an additional special test-suite comment. The patch now explains its IPv4 binding assumption next to the probe.

The submission will contain the shell patch, without the Python verifier or a new Python test dependency. The existing serv-udp.sh supplies the functional success check. Running it through the six-second startup wrapper supplies a reproducible regression case: it fails before the fix and passes after it. The local verifier was used to validate socket-output parsing and the helper's success, process-exit, skip-cleanup, and timeout branches. Those branch checks are local evidence, not new automated coverage in the upstream suite; the MR must state that distinction.

No dedicated unit-test harness for these shell helpers was found in the 3.8.13 tests inspected. That does not establish that Python cannot be used upstream; keeping this submission dependency-free is a scope choice. Use the existing test and before/after reproduction as the submission's coverage argument, retaining the platform limitations above. Apply the patch in an upstream checkout and include those results with the contributor's own sign-off. No MR or sign-off has been created.

Local NixOS integration and build results

overlays/default.nix imports the overlays/patches overlay, which loads gnutls/default.nix to apply the patch and keep serv-udp.sh enabled. The patch itself has no Nix dependencies and applies to 3.8.13 and GitLab master without fuzz.

The final patch was rebuilt with:

nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.gnutls

That x86-64-v3 build passed: 927 tests, 796 passes, 131 existing skips, zero failures/errors, and PASS: serv-udp.sh. The patch bytes in the built derivation were compared with the repository artifact; both have SHA-256 59013d47fd446f2dd065012a2259ccc1898fedc8a053a630e13efa0076368760. All seven local checks passed, including skip cleanup and exactly 90 probes with 89 sleeps on timeout. The six-second before/after reproduction was also repeated successfully with the final helper.