From c1865c7472d020a39d61a698cb109c91ced39a01 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Oct 2026 21:24:05 -0700 Subject: [PATCH 01/12] FEATURE-ci-v3-runtime-green: remove PSK fixture scheduling races --- ChangeLog | 2 ++ specs/tasks/FEATURE-ci-v3-runtime-green.md | 21 +++++++++++++++++++++ test/unit/tls_psk_fixture.hpp | 13 ++++++++----- test/unit/tls_psk_test.cpp | 12 ++++++------ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/ChangeLog b/ChangeLog index 766cdb7ee..f8df31b2e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ Version 3.0.0 - 2026-10-10 + Keep PSK cancellation lookup held until owner release and avoid artificial + per-fragment scheduling delays in the TLS test transport. Avoid Windows DLL shutdown hangs by retaining the default timer service during process termination while preserving ordinary timer cleanup. Report the saved Winsock error code when host WSAPoll fails. diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 3117565eb..4a5bb1863 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -70,3 +70,24 @@ Integrate the focused repairs and ChangeLog update now. Full CI remains incomple Groundwork validation has not run and stays deferred until full CI is green. This integration does not mark the overall CI repair complete or reopen closed diagnostic batches. Draft PR #382 carries the original working-branch runs. + +## Stage 4: PSK test fixture scheduling (2026-10-27) + +- [x] Read the completed, previously authorized diagnostic receipts; keep their budgets closed. +- [x] Preserve the native Darwin RED: 14 tests/313 checks, three failures; held TLS1.3 cancellation callback already returned, and certificate-default TLS1.3 handshake timed out. +- [x] Release the held cancellation lookup explicitly after assertions and close; bound the callback wait to five seconds for failure cleanup. +- [x] Drain ready fragmented transport work and yield while waiting for provider work, preserving the original fragments and every deadline/assertion. +- [x] Reproduce the real certificate fixture timeout under coarse scheduling, then pass the same regression and all 14 PSK tests/313 checks. +- [x] Pass changed-file cpplint and diff checks. +- [ ] Native current-source CI, including Darwin selected archive acceptance. +- [ ] Resolve the remaining Windows external-loop failure from supported evidence. +- [ ] Close the FreeBSD selected-archive timeout gate with actual evidence. + +Evidence and commands: primary checkout `.debug/v3-ci-native-repairs-PJFEkn/journal.md`. +Current c1 installed matrix passed Linux, Darwin, FreeBSD; Windows failed. Native/fuzz, CodeQL and Verify Build passed. Source-equivalent selected archive remains failed; no current-source green claim. + +The diagnostic Windows four-test program failed one of 52 checks in 0.741 seconds with WSAPoll error10038 before stop was requested. Two bounded new socket-only probes on local Windows11 returned POLLNVAL successfully; they do not establish the Server2022 failure cause. No poll error suppression or guessed production fix was applied. Previous capture budgets remain closed. + +Fixture root cause: the cancellation rescue unconditionally released after250ms, before the owner could assert cancellation under slower scheduling. The transport also slept after every41/73-byte fragment, charging scheduler delays to real handshake deadlines. A minimal real TLS1.3 certificate fixture with1ms sleeps delayed to15ms failed at2089ms/outcome8(timeout); draining progress first passed at625ms. An intermediate full coarse-scheduling check still failed two held/capacity assertions because idle sleeps could expire the400ms pre-lookup deadline. Yielding while idle preserved that deadline and passed all313 checks in1.029seconds, with zero skips. The100ms owner-responsiveness assertion, cancellation/timeout/capacity/late-result assertions, bounded drains and all protocol settings remain unchanged. Only test source changed; production TLS is unchanged. + +Setup evidence is retained honestly: system OpenSSL was rejected before selecting the existing3.5.9 provider; the first suite launch used build-root cwd instead of test cwd and failed fixture path lookup. Correcting cwd, as required by the generated relative TLS_TEST_DIR, produced the meaningful behavioral check. Neither setup failure justifies a source workaround. No broad baseline or Groundwork validation was run. diff --git a/test/unit/tls_psk_fixture.hpp b/test/unit/tls_psk_fixture.hpp index c8ca74d67..3c4784763 100644 --- a/test/unit/tls_psk_fixture.hpp +++ b/test/unit/tls_psk_fixture.hpp @@ -135,17 +135,20 @@ struct connection : mtls_test::adapter_connection { if (host) SSL_set_tlsext_host_name(peer.get(), host); handshake = hd::tls_handshake_operation(owner, 1, std::chrono::steady_clock::now() + timeout); } - void tick() { - ex.run_pending(); - for (const auto& op : raw.pending_ops()) transfer(op); + bool tick() { + bool progressed = ex.run_pending() != 0; + for (const auto& op : raw.pending_ops()) progressed = transfer(op) || progressed; advance_peer(); + return progressed || ex.pending() != 0; } template bool until(Predicate predicate, std::chrono::milliseconds timeout = std::chrono::seconds(3)) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { - tick(); + const bool progressed = tick(); if (predicate()) return true; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // Fragmentation is deliberate; adding a scheduling delay to every + // ready fragment can consume the real handshake deadline. + if (!progressed) std::this_thread::yield(); } return false; } diff --git a/test/unit/tls_psk_test.cpp b/test/unit/tls_psk_test.cpp index 72a642fb5..e59e24d34 100644 --- a/test/unit/tls_psk_test.cpp +++ b/test/unit/tls_psk_test.cpp @@ -119,23 +119,23 @@ LT_BEGIN_AUTO_TEST(psk_suite, blocking_lookup_leaves_owner_responsive_and_cancel auto released = release.get_future().share(); auto config = psk_test::credentials(runtime, [&](auto, const auto&) { entered = true; - released.wait(); + // The owner releases lookup only after checking cancellation. + // A broken blocking implementation must still unwind the fixture. + released.wait_for(5s); returned = true; return hd::psk_lookup_result{hd::psk_lookup_status::accepted, hd::secure_bytes(psk_test::bytes("secret"))}; }); hd::tls_credentials_registry registry; LT_ASSERT(registry.replace(config).ok()); psk_test::connection peer(registry.acquire()->select_default(), version, "identity", psk_test::bytes("secret")); - // Bounded rescue ensures RED cannot leave a fixture hanging in lookup. - auto rescue = std::async(std::launch::async, [&] { std::this_thread::sleep_for(250ms); release.set_value(); }); peer.handshake.submit(*peer.tls); std::chrono::steady_clock::duration longest{}; const auto until = std::chrono::steady_clock::now() + 1s; while (!entered && std::chrono::steady_clock::now() < until) { const auto before = std::chrono::steady_clock::now(); - peer.tick(); + const bool progressed = peer.tick(); longest = std::max(longest, std::chrono::steady_clock::now() - before); - std::this_thread::sleep_for(1ms); + if (!progressed) std::this_thread::yield(); } const auto cancelled = peer.tls->request_cancel(*peer.handshake.state()); peer.ex.run_pending(); @@ -147,7 +147,7 @@ LT_BEGIN_AUTO_TEST(psk_suite, blocking_lookup_leaves_owner_responsive_and_cancel LT_CHECK(peer.handshake.state()->stored_result().code == hh::outcome_code::cancelled); LT_CHECK(!peer.tls->peer_metadata()); peer.close(); - rescue.get(); + release.set_value(); runtime->stop(); LT_CHECK(runtime->drain(std::chrono::steady_clock::now() + 1s)); peer.ex.run_pending(); From bc657d4794d7e94c7a3d9e2d688f96cee1eb2da3 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Oct 2026 22:15:02 -0700 Subject: [PATCH 02/12] FEATURE-ci-v3-runtime-green: bind Windows allocation faults to static proof --- ChangeLog | 2 ++ scripts/lib/package_integration.py | 9 +++-- scripts/lib/package_matrix.py | 12 +++++++ scripts/test-v3-installed-consumer.py | 27 ++++++++++++++ specs/tasks/FEATURE-ci-v3-runtime-green.md | 23 +++++++++++- test/installed/v3-package-matrix.json | 36 +++++++++++++++++++ test/unit/external_readiness_adapter_test.cpp | 9 +++++ 7 files changed, 114 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index f8df31b2e..1227cdead 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ Version 3.0.0 - 2026-10-10 + Run Windows allocation-failure injection against the installed static + library and require ordinary readiness cases in both linkage lanes. Keep PSK cancellation lookup held until owner release and avoid artificial per-fragment scheduling delays in the TLS test transport. Avoid Windows DLL shutdown hangs by retaining the default timer service diff --git a/scripts/lib/package_integration.py b/scripts/lib/package_integration.py index f2462e87f..71ca28df4 100644 --- a/scripts/lib/package_integration.py +++ b/scripts/lib/package_integration.py @@ -38,6 +38,11 @@ def run_suites(inventory, host, mode, linkage, directory, prefix, source, build, receipt = dict(name=name, tls=mode, linkage=linkage, execution=definition.get('execution', 'installed-library')) try: flags = cflags + ['-DHTTPSERVER_COMPILATION', '-I' + str(source / 'src'), '-I' + str(source / 'test')] + if host == 'Windows' and linkage == 'shared' and definition.get('windows_static_cases'): + # An executable's operator new cannot interpose PE DLL imports. + # The installed static lane retains every fault-injection case. + flags += ['-DLHS_TEST_DLL_LINKAGE'] + receipt['windows_static_cases'] = definition['windows_static_cases'] for macro, value in (('TLS_TEST_DIR', source / 'test'), ('PARITY_TRANSCRIPT_DIR', source / 'test/parity/transcripts'), ('PARITY_DATA_ROOT', source / 'test'), ('HTTP2_CORPUS_DIR', source / 'test/conformance/http2')): flags += ['-D' + macro + '="' + value.as_posix() + '"'] @@ -61,9 +66,7 @@ def run_suites(inventory, host, mode, linkage, directory, prefix, source, build, started = time.monotonic() output = command([str(executable)], source / 'test', env, timeout=180, allow_failure=True) receipt.update(matrix.suite_result(output, command.records[-1]['exit'])) - missing = set(definition.get('required_cases', [])) - set(receipt['case_names']) - if missing: - raise ValueError('required suite cases absent: ' + ', '.join(sorted(missing))) + matrix.check_suite_cases(definition, host, linkage, receipt['case_names']) receipt.update(duration=time.monotonic() - started, log=command.records[-1]['log']) if name in ('tls_io_loopback', 'native_http1_tls_parity'): matrix.check_tls_witnesses(output, inventory['platforms'][host]) diff --git a/scripts/lib/package_matrix.py b/scripts/lib/package_matrix.py index 3a86b8508..3cfae3e31 100644 --- a/scripts/lib/package_matrix.py +++ b/scripts/lib/package_matrix.py @@ -150,6 +150,18 @@ def suites(inventory, platform, tls): return {name: inventory['suites'][name] for name in names} +def check_suite_cases(definition, host, linkage, observed): + required = set(definition.get('required_cases', [])) + windows_static = set(definition.get('windows_static_cases', [])) + if not windows_static <= required: + raise ValueError('Windows static cases must also be required suite cases') + if host == 'Windows' and linkage == 'shared': + required -= windows_static + missing = required - set(observed) + if missing: + raise ValueError('required suite cases absent: ' + ', '.join(sorted(missing))) + + def suite_result(output, status): output = output.replace('\r\n', '\n') tests = re.findall(r'^(\d+) tests executed$', output, re.M) diff --git a/scripts/test-v3-installed-consumer.py b/scripts/test-v3-installed-consumer.py index 977611e8d..2f4aceb76 100644 --- a/scripts/test-v3-installed-consumer.py +++ b/scripts/test-v3-installed-consumer.py @@ -1580,5 +1580,32 @@ def test_nonzero_shutdown_is_failure(self): self.assertEqual(8, json.loads(receipt.read_text())['exit']) +class SuiteCaseCoverageTest(unittest.TestCase): + def setUp(self): + self.definition = dict(required_cases=['ordinary', 'allocation_failure'], + windows_static_cases=['allocation_failure']) + + def test_windows_dll_requires_all_ordinary_cases(self): + matrix.check_suite_cases(self.definition, 'Windows', 'shared', ['ordinary']) + with self.assertRaisesRegex(ValueError, 'ordinary'): + matrix.check_suite_cases(self.definition, 'Windows', 'shared', []) + + def test_static_proof_requires_all_allocation_failure_cases(self): + for host in ('Windows', 'Linux', 'Darwin', 'FreeBSD'): + with self.subTest(host=host), self.assertRaisesRegex(ValueError, 'allocation_failure'): + matrix.check_suite_cases(self.definition, host, 'static', ['ordinary']) + matrix.check_suite_cases(self.definition, host, 'static', ['ordinary', 'allocation_failure']) + + def test_other_shared_platforms_retain_allocation_failure_coverage(self): + for host in ('Linux', 'Darwin', 'FreeBSD'): + with self.subTest(host=host), self.assertRaisesRegex(ValueError, 'allocation_failure'): + matrix.check_suite_cases(self.definition, host, 'shared', ['ordinary']) + + def test_windows_static_cases_must_also_be_required(self): + self.definition['required_cases'] = ['ordinary'] + with self.assertRaisesRegex(ValueError, 'static.*required'): + matrix.check_suite_cases(self.definition, 'Windows', 'static', ['ordinary']) + + if __name__ == '__main__': unittest.main() diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 4a5bb1863..882b37e20 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -1,6 +1,6 @@ # FEATURE-ci-v3-runtime-green -Status: Stages 1 and 3 implemented with focused proof; complete CI and remaining supported runtime repairs are pending. Groundwork validation is explicitly prohibited until FULL CI IS GREEN. +Status: Stages 1, 3, 4 and 5 implemented with focused proof; complete CI and remaining supported runtime repairs are pending. Groundwork validation is explicitly prohibited until FULL CI IS GREEN. Base: `v3` at `8f3083977f49706e66cefbb3ee8b6f6685469719`. Working branch: `task/FEATURE-ci-v3-runtime-green`. User requested integration into `v3` before full CI completion; retain incomplete verification explicitly. Plan: primary checkout `.groundwork-plans/FEATURE-ci-v3-runtime-green-plan.md`. @@ -91,3 +91,24 @@ The diagnostic Windows four-test program failed one of 52 checks in 0.741 second Fixture root cause: the cancellation rescue unconditionally released after250ms, before the owner could assert cancellation under slower scheduling. The transport also slept after every41/73-byte fragment, charging scheduler delays to real handshake deadlines. A minimal real TLS1.3 certificate fixture with1ms sleeps delayed to15ms failed at2089ms/outcome8(timeout); draining progress first passed at625ms. An intermediate full coarse-scheduling check still failed two held/capacity assertions because idle sleeps could expire the400ms pre-lookup deadline. Yielding while idle preserved that deadline and passed all313 checks in1.029seconds, with zero skips. The100ms owner-responsiveness assertion, cancellation/timeout/capacity/late-result assertions, bounded drains and all protocol settings remain unchanged. Only test source changed; production TLS is unchanged. Setup evidence is retained honestly: system OpenSSL was rejected before selecting the existing3.5.9 provider; the first suite launch used build-root cwd instead of test cwd and failed fixture path lookup. Correcting cwd, as required by the generated relative TLS_TEST_DIR, produced the meaningful behavioral check. Neither setup failure justifies a source workaround. No broad baseline or Groundwork validation was run. + +## Stage 5: Windows allocator fault-injection fixture linkage + +- [x] Identify the current PR383 failed program from one bounded selected artifact assessment (3,891,726 range bytes; no full ZIP or original execution). +- [x] Prove that executable operator-new replacement does not affect Windows DLL allocation, and that static linkage delivers the intended fault. +- [x] Keep all 22 ordinary readiness cases against the installed Windows DLL; require all 27 cases, including the five unchanged allocation-failure cases, against the installed static archive. +- [x] Make the required case names explicit in the inventory and reject every missing static fault case. Other platforms retain their original fault coverage. +- [x] Pass native Windows focused DLL22/438 and static27/486 gates, Linux28/497, 28 package coverage/matrix checks, changed-file lint and diff checks. +- [ ] Full exact-source CI with the corrected installed linkage profiles. + +Current PR383 package job122630892885 passed native62 in4.812276s and external4 in0.572681s, then failed external_readiness_adapter27/486 with13 failures, all inside five allocation-failure cases. The assertion expecting bad_alloc was false and regular work completed. The PSK change cannot affect these allocation boundaries; this repair is within the accepted overall CI goal. Member CRC/SHA are verified; the full212MB ZIP digest is metadata-only. Assessment is closed. + +A tiny native PE fixture reproduces the cause: executable allocation throws under its replacement operator new, DLL-owned allocation does not (exit1/0.362s). Linking the same owner object into the executable makes both throw (exit0/0.309s). Microsoft documents per-module DLL import binding: https://learn.microsoft.com/en-us/cpp/build/linking-an-executable-to-a-dll?view=msvc-170 . The chosen correction scopes allocation injection to linkage where it can actually reach the owner. Artificially throwing in the caller would not exercise the backend; a new production allocator hook or loader patch is unnecessary. + +Only Windows shared compilation selects LHS_TEST_DLL_LINKAGE; this omits allocator replacement and the five fault registrations from that executable. All five original assertion bodies remain unchanged and mandatory in the installed static lane. The inventory names all27 ordinary/static cases, and the receipt validator requires the22 DLL cases or all27 static cases. No test skip or failed/empty receipt is accepted. Every original C++ assertion line is preserved. The other platforms keep all five fault cases in both linkage lanes. + +Fresh current-source focused Windows backend DLL (16 owning translation units) passed22/438 in0.899s; matching static executable passed27/486 in1.237s, both zero skips and retired. Linux passes28/497 including its descriptor-zero case. Removing any one of the five fault names from the static receipt is rejected. Python coverage/matrix tests pass28 cases. Native setup initially missed net_address.cpp at link; adding that existing owning translation unit corrected the test build without a production workaround. Context is qualified local Windows11/cached LLVM23.1.4/GCC15.2 headers and retained runtime DLLs, not Server2022/MSYS2 CI. Evidence: primary `.debug/v3-ci-native-repairs-PJFEkn/allocator-linkage/`. + +## Approved Windows capture closure + +User approved exact430fa17e publication and ONE1/60 Windows original external-loop capture. Run40635371672 completed once:4tests/52checks/zero skips, exit0 in0.4701966s, retirement confirmed, stderr empty. Budget is CLOSED; no retry or remaining-time authority. This establishes nonreproduction in that context, not a fix or independence of the earlier WSAPoll10038 failure. The diagnostic branch remains NEVER MERGE. Only a proven repair may change production behavior. diff --git a/test/installed/v3-package-matrix.json b/test/installed/v3-package-matrix.json index 210a368cb..3811a44cd 100644 --- a/test/installed/v3-package-matrix.json +++ b/test/installed/v3-package-matrix.json @@ -73,6 +73,42 @@ "external_readiness_adapter": { "sources": [ "unit/external_readiness_adapter_test.cpp" + ], + "required_cases": [ + "host_drives_read_write_and_snapshot_is_owned", + "spurious_unknown_and_wrong_generation_do_not_consume", + "omitted_then_republished_registration_rejects_old_callbacks", + "reopen_same_id_and_descriptor_rejects_every_old_flag_combination", + "detached_read_cannot_rearm_into_reopened_id", + "detached_accept_cannot_rearm_into_reopened_id", + "exclusive_retirement_expires_real_timer_without_host_dispatch", + "timers_use_host_clock_order_and_reject_decreasing_samples", + "timer_cancel_expiry_race_is_exactly_once", + "wake_is_doorbell_and_post_snapshot_work_survives_acknowledgement", + "overlapping_dispatch_rejects_without_consuming_valid_read", + "host_accept_and_close_deliver_once", + "wake_published_during_dispatch_survives_and_recursive_overlap_reject", + "release_during_completion_defers_close_and_cannot_step_replacement", + "hard_wake_failure_stops_and_unwinds_pending_work", + "decreasing_clock_rejects_a_valid_socket_event_before_io", + "exhausted_identity_never_wraps_and_closes_unregistered_socket", + "buffered_read_precedes_combined_close_error_and_duplicate_callback", + "complete_snapshots_prune_retired_connection_records", + "allocation_failure_read_batch_remains_owned_until_close", + "allocation_failure_write_batch_remains_owned_until_close", + "allocation_failure_accept_batch_remains_owned_until_close", + "allocation_failure_second_direction_keeps_first_direction_owned", + "allocation_failure_due_timers_remain_owned_until_close", + "exhausted_generation_fails_snapshot_and_shutdown_completes_once", + "successful_accept_after_listener_retirement_closes_fresh_handle", + "successful_accept_after_backend_close_closes_fresh_handle" + ], + "windows_static_cases": [ + "allocation_failure_read_batch_remains_owned_until_close", + "allocation_failure_write_batch_remains_owned_until_close", + "allocation_failure_accept_batch_remains_owned_until_close", + "allocation_failure_second_direction_keeps_first_direction_owned", + "allocation_failure_due_timers_remain_owned_until_close" ] }, "readiness_contract": { diff --git a/test/unit/external_readiness_adapter_test.cpp b/test/unit/external_readiness_adapter_test.cpp index e10a216e2..70ee2601c 100644 --- a/test/unit/external_readiness_adapter_test.cpp +++ b/test/unit/external_readiness_adapter_test.cpp @@ -39,6 +39,7 @@ #include "./io_backend_contract.hpp" #include "./littletest.hpp" +#ifndef LHS_TEST_DLL_LINKAGE // Fault injection is confined to this test executable and the dispatching // thread. Restore allocation before asserting or shutting down the backend. namespace { @@ -79,6 +80,7 @@ void* operator new(std::size_t size) { void operator delete(void* memory) noexcept { std::free(memory); } void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } #endif +#endif // LHS_TEST_DLL_LINKAGE namespace srv = httpserver::server; namespace { @@ -203,6 +205,7 @@ void detached_reopen(littletest::test_runner* __lt_tr__, const char* __lt_name__ LT_CHECK_EQ(fx.backend.pending_count(), std::size_t{0}); } +#ifndef LHS_TEST_DLL_LINKAGE void failed_direction_collection(littletest::test_runner* __lt_tr__, const char* __lt_name__, hd::io_op_kind kind) { fixture fx; @@ -253,6 +256,8 @@ void failed_direction_collection(littletest::test_runner* __lt_tr__, LT_CHECK_EQ(second.delivered.load(), 1); } +#endif // LHS_TEST_DLL_LINKAGE + void accepted_after_teardown(littletest::test_runner* __lt_tr__, const char* __lt_name__, bool shutdown) { fixture fx; @@ -855,6 +860,9 @@ LT_BEGIN_AUTO_TEST(external_adapter_suite, complete_snapshots_prune_retired_conn } LT_END_AUTO_TEST(complete_snapshots_prune_retired_connection_records) +#ifndef LHS_TEST_DLL_LINKAGE +// PE DLL allocations bind their own allocator imports. These cases run against +// the installed static library, where this executable's override reaches them. LT_BEGIN_AUTO_TEST(external_adapter_suite, allocation_failure_read_batch_remains_owned_until_close) failed_direction_collection(__lt_tr__, __lt_name__, hd::io_op_kind::read); LT_END_AUTO_TEST(allocation_failure_read_batch_remains_owned_until_close) @@ -923,6 +931,7 @@ LT_BEGIN_AUTO_TEST(external_adapter_suite, allocation_failure_due_timers_remain_ LT_CHECK_EQ(first.delivered.load(), 1); LT_CHECK_EQ(second.delivered.load(), 1); LT_END_AUTO_TEST(allocation_failure_due_timers_remain_owned_until_close) +#endif // LHS_TEST_DLL_LINKAGE LT_BEGIN_AUTO_TEST(external_adapter_suite, exhausted_generation_fails_snapshot_and_shutdown_completes_once) fixture fx; From 9c92e55a4009680104f2b7fc22fd3bbed4a45bb3 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Oct 2026 22:18:32 -0700 Subject: [PATCH 03/12] FEATURE-ci-v3-runtime-green: share linkage coverage in archive receipt import --- scripts/lib/release_platform.py | 2 +- specs/tasks/FEATURE-ci-v3-runtime-green.md | 2 + test/integ/release_platform_bundle_test.py | 46 +++++++++++++++++----- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/scripts/lib/release_platform.py b/scripts/lib/release_platform.py index 6b44e1c1b..766841d9e 100644 --- a/scripts/lib/release_platform.py +++ b/scripts/lib/release_platform.py @@ -153,7 +153,7 @@ def package_origin(root, origin, read_json, read_text, expected, platform, read_ receipt = suites[name] observed = matrix.suite_result(read_text(receipt['log']), 0) require(all(receipt.get(k) == v for k, v in observed.items()), 'stale platform suite summary') - require(set(definition.get('required_cases', [])) <= set(observed['case_names']), 'pruned platform suite') + matrix.check_suite_cases(definition, platform, linkage, observed['case_names']) return result diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 882b37e20..84a0a51ca 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -112,3 +112,5 @@ Fresh current-source focused Windows backend DLL (16 owning translation units) p ## Approved Windows capture closure User approved exact430fa17e publication and ONE1/60 Windows original external-loop capture. Run40635371672 completed once:4tests/52checks/zero skips, exit0 in0.4701966s, retirement confirmed, stderr empty. Budget is CLOSED; no retry or remaining-time authority. This establishes nonreproduction in that context, not a fix or independence of the earlier WSAPoll10038 failure. The diagnostic branch remains NEVER MERGE. Only a proven repair may change production behavior. + +Stage5 receipt-import closure: source review found the selected-archive importer independently required all cases in each linkage. A new real receipt-bundle regression was RED with `valid DLL/static split rejected: pruned platform suite`. Reusing the same coverage validator fixes this producer/consumer mismatch while still rejecting missing static fault cases. All14 platform-bundle tests and changed Python syntax/diff checks pass. This is part of the same proven setup repair; native binaries are unchanged, so their focused evidence is carried without repetition. Current-source CI remains pending. diff --git a/test/integ/release_platform_bundle_test.py b/test/integ/release_platform_bundle_test.py index c646844c0..2e8a66000 100755 --- a/test/integ/release_platform_bundle_test.py +++ b/test/integ/release_platform_bundle_test.py @@ -132,7 +132,7 @@ def test_valid_archive_import_rejects_duplicate_members_and_namespace_collisions stream.add(root/'Darwin/catalog.json',arcname='Darwin/catalog.json') with self.assertRaises(ValueError): module.unpack(duplicate,root/'duplicate') - def platform_origin_fixture(self): + def platform_origin_fixture(self, platform='Darwin'): module = self.module() import package_matrix as matrix import release_artifact as identity @@ -147,7 +147,7 @@ def write(path, value): path.write_text(json.dumps(value)) header = root / 'provider.h'; header.write_bytes(b'selected OpenSSL\r\n') selected = dict(archive_sha256='a'*64, snapshot_sha256='b'*64, extracted_sha256=hashlib.sha256(json.dumps({},sort_keys=True).encode()).hexdigest(), tls='both', sanitizer='none') - write(package/'summary.json', dict(status='passed', host='Darwin', lanes={'no':{},'yes':{}}, + write(package/'summary.json', dict(status='passed', host=platform, lanes={'no':{},'yes':{}}, revision='d'*40, provider_header_sha256=identity.sha256(header), release_artifact={'archive':{'sha256':'a'*64}, 'source':{'snapshot_sha256':'b'*64,'head':'d'*40}})) for mode in ('no','yes'): @@ -160,7 +160,7 @@ def write(path, value): public[linkage]=dict(dependencies=graph, compile={'dependencies':[]}, cells=[{'configurations':inventory['cases']},{'configurations':inventory['cases']}]) suites={} - for name, definition in matrix.suites(inventory, 'Darwin', mode).items(): + for name, definition in matrix.suites(inventory, platform, mode).items(): cases=definition.get('required_cases',[]) or ['fixture_case'] log=lane/('integration-'+linkage)/(name+'.log') log.parent.mkdir(parents=True, exist_ok=True) @@ -171,28 +171,56 @@ def write(path, value): write(lane/'audit.json',dict(violations=[],tls_mode=mode,public=public)) origin=dict(package=str(package), source_root=str(root/'source'), source_commit='d'*40, provider_header=str(header), native={}) # This test checks installed matrix origin independently; native origin is mandatory in complete bundles. - counts=module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), lambda p: Path(p).read_text(), selected, 'Darwin') + counts=module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), lambda p: Path(p).read_text(), selected, platform) self.assertGreater(counts['tests'],0) wrong_origin=dict(origin,source_commit='e'*40) with self.assertRaises(ValueError): - module.package_origin(ROOT,wrong_origin,lambda p:json.loads(Path(p).read_text()),lambda p:Path(p).read_text(),selected,'Darwin') + module.package_origin(ROOT,wrong_origin,lambda p:json.loads(Path(p).read_text()),lambda p:Path(p).read_text(),selected,platform) for mode in ('no','yes'): build=root/('native-'+mode); (build/'test').mkdir(parents=True) names=['protocol_fuzz_replay','hpack_corpus','http2_fuzz_replay','quic_fuzz_replay', - 'quic_recovery_fuzz_replay','http3_fuzz_replay','qpack_fuzz_replay','io_kqueue_backend'] + 'quic_recovery_fuzz_replay','http3_fuzz_replay','qpack_fuzz_replay','io_' + inventory['platforms'][platform] + '_backend'] if mode=='yes': names.append('quic_tls_callbacks_fuzz_replay') for name in names: (build/'test'/(name+'.trs')).write_text(':test-result: PASS\n') (build/'test/test-suite.log').write_text('# TOTAL: %d\n# PASS: %d\n# SKIP: 0\n# XFAIL: 0\n# FAIL: 0\n# XPASS: 0\n# ERROR: 0\n'%(len(names),len(names))) origin['native'][mode]=dict(build=str(build), tests=names) counts=checks.counts(counts['tests']+len(names),counts['checks']+len(names)) artifacts=[dict(path=str(header),sha256=identity.sha256(header),bytes=header.stat().st_size)] - row=dict(id='Darwin',identity=selected,archive_sha256='a'*64,origin=origin,status='passed',artifacts=artifacts, - receipt=dict(status='passed',gate='Darwin',identity=selected,counts=counts,artifacts=artifacts)) + row=dict(id=platform,identity=selected,archive_sha256='a'*64,origin=origin,status='passed',artifacts=artifacts, + receipt=dict(status='passed',gate=platform,identity=selected,counts=counts,artifacts=artifacts)) retained=root/'retained' - catalog=module.retain(retained,'Darwin',row,[p for p in root.rglob('*') if p.is_file()]) + catalog=module.retain(retained,platform,row,[p for p in root.rglob('*') if p.is_file()]) module.validate_origin(ROOT,row,catalog) return module, root, package, header, selected, origin, row, catalog + def test_windows_origin_retains_static_fault_proof_and_accepts_dll_profile(self): + import package_matrix as matrix + module, _, package, _, selected, origin, _, _ = self.platform_origin_fixture('Windows') + definition = matrix.load()['suites']['external_readiness_adapter'] + + def rewrite(linkage, cases): + directory = package / 'tls-no' / ('integration-' + linkage) + suites_path = directory / 'suites.json' + suites = json.loads(suites_path.read_text()) + receipt = suites['external_readiness_adapter'] + log = Path(receipt['log']) + log.write_text(''.join('Running test (%d): %s\n' % (i, case) for i, case in enumerate(cases)) + + '%d tests executed\n%d checks\n-> 0 failures\n-> 0 skipped\n' % (len(cases), len(cases))) + receipt.update(matrix.suite_result(log.read_text(), 0)) + suites_path.write_text(json.dumps(suites)) + + ordinary = [name for name in definition['required_cases'] if name not in definition['windows_static_cases']] + rewrite('shared', ordinary) + try: + module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), + lambda p: Path(p).read_text(), selected, 'Windows') + except ValueError as error: + self.fail('valid DLL/static split rejected: ' + str(error)) + rewrite('static', ordinary) + with self.assertRaisesRegex(ValueError, 'required suite cases absent'): + module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), + lambda p: Path(p).read_text(), selected, 'Windows') + def test_platform_origin_rejects_pruned_mandatory_native_suite(self): import release_checks as checks module, root, _, _, _, _, row, catalog = self.platform_origin_fixture() From b71192c918dffca48fa7a971795160b63d3d199f Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 27 Oct 2026 23:05:18 -0700 Subject: [PATCH 04/12] FEATURE-ci-v3-runtime-green: register native executable tests once --- ChangeLog | 1 + specs/tasks/FEATURE-ci-v3-runtime-green.md | 16 +++++++++++++++- test/Makefile.am | 4 ---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1227cdead..bd7c15dc2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,5 @@ Version 3.0.0 - 2026-10-10 + Register each native executable once in Automake test evidence inventories. Run Windows allocation-failure injection against the installed static library and require ordinary readiness cases in both linkage lanes. Keep PSK cancellation lookup held until owner release and avoid artificial diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 84a0a51ca..326659289 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -1,6 +1,6 @@ # FEATURE-ci-v3-runtime-green -Status: Stages 1, 3, 4 and 5 implemented with focused proof; complete CI and remaining supported runtime repairs are pending. Groundwork validation is explicitly prohibited until FULL CI IS GREEN. +Status: Stages 1, 3, 4, 5 and 6 implemented with focused proof; complete CI and remaining supported runtime repairs are pending. Groundwork validation is explicitly prohibited until FULL CI IS GREEN. Base: `v3` at `8f3083977f49706e66cefbb3ee8b6f6685469719`. Working branch: `task/FEATURE-ci-v3-runtime-green`. User requested integration into `v3` before full CI completion; retain incomplete verification explicitly. Plan: primary checkout `.groundwork-plans/FEATURE-ci-v3-runtime-green-plan.md`. @@ -114,3 +114,17 @@ Fresh current-source focused Windows backend DLL (16 owning translation units) p User approved exact430fa17e publication and ONE1/60 Windows original external-loop capture. Run40635371672 completed once:4tests/52checks/zero skips, exit0 in0.4701966s, retirement confirmed, stderr empty. Budget is CLOSED; no retry or remaining-time authority. This establishes nonreproduction in that context, not a fix or independence of the earlier WSAPoll10038 failure. The diagnostic branch remains NEVER MERGE. Only a proven repair may change production behavior. Stage5 receipt-import closure: source review found the selected-archive importer independently required all cases in each linkage. A new real receipt-bundle regression was RED with `valid DLL/static split rejected: pruned platform suite`. Reusing the same coverage validator fixes this producer/consumer mismatch while still rejecting missing static fault cases. All14 platform-bundle tests and changed Python syntax/diff checks pass. This is part of the same proven setup repair; native binaries are unchanged, so their focused evidence is carried without repetition. Current-source CI remains pending. + +## Stage 6: unique native Automake inventory + +- [x] Preserve Darwin archive failure `duplicate/empty native suite inventory` after its native commands completed. +- [x] Reproduce the exact producer inventory command against a real configured Makefile: TLSyes271 entries/266 unique, five duplicated executables. +- [x] Remove only four redundant TESTS additions; keep each executable in check_PROGRAMS, which already feeds TESTS recursively. +- [x] Regenerate actual Automake/configured Makefiles and verify unique inventories in both TLS modes: yes266/266, no235/235. All five affected executables remain registered exactly once. +- [ ] Full current-source archive evidence with this registration correction. + +The duplicated names are http_cookie, consumer_v3_cookie, consumer_v3_cookie_wire, protocol_limits and protocol_compression. TESTS already equals $(check_PROGRAMS); the extra TESTS += lines counted them twice. The strict receipt duplicate rejection is unchanged. No executable, assertion, script test, protocol setting or failure deadline was removed. This four-line configuration repair is proved by the actual producer command, not a source-text mirror test or a broad baseline rerun. + +Evidence: primary `.debug/v3-ci-native-repairs-PJFEkn/native-inventory/`. Autoreconf succeeded with existing AC_OUTPUT/.PHONY warnings. Initial config.status invocation used the source directory instead of its configured build directory and failed before any native execution; rechecking/regenerating in the correct build directory fixed setup. No unrelated source or generated tracked file changed. + +Current9c92 Windows package job122646921257 fails earlier at the public shared external consumer: exit6, captured stderr WSAPoll error10038. The selected820-byte process receipt was verified within a closed2,581,790-byte range assessment; no full artifact or original program ran. It contains no failed-call slot/state values. The earlier approved external4 capture remains CLOSED/PASS4/52/nonreproduction, and does not authorize running the distinct public consumer. No guessed polling or shutdown suppression was applied. A fresh consumer-specific bounded capture proposal is needed if further native diagnosis is required. diff --git a/test/Makefile.am b/test/Makefile.am index c8df93548..d0daa7085 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1588,7 +1588,6 @@ consumer_v3_peer_policy_runtime_LDADD = $(top_builddir)/src/libhttpserver_v3core consumer_v3_peer_policy_runtime_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) check_PROGRAMS += http_cookie consumer_v3_cookie -TESTS += http_cookie consumer_v3_cookie http_cookie_SOURCES = unit/http_cookie_test.cpp http_cookie_LDADD = $(top_builddir)/src/libhttpserver.la $(V3_TLS_LIBS) http_cookie_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) @@ -1598,7 +1597,6 @@ consumer_v3_cookie_LDADD = $(top_builddir)/src/libhttpserver.la $(V3_TLS_LIBS) consumer_v3_cookie_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) check_PROGRAMS += consumer_v3_cookie_wire -TESTS += consumer_v3_cookie_wire consumer_v3_cookie_wire_SOURCES = integ/consumer_v3_cookie_wire.cpp consumer_v3_cookie_wire_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/test -I$(top_srcdir)/examples consumer_v3_cookie_wire_LDADD = $(top_builddir)/src/libhttpserver.la $(V3_TLS_LIBS) @@ -1646,11 +1644,9 @@ compiled_operations_CPPFLAGS = -I$(top_srcdir)/src compiled_operations_LDADD = $(top_builddir)/src/libhttpserver.la check_PROGRAMS += protocol_limits -TESTS += protocol_limits protocol_limits_SOURCES = unit/protocol_limits_test.cpp check_PROGRAMS += protocol_compression -TESTS += protocol_compression protocol_compression_SOURCES = unit/protocol_compression_test.cpp TESTS += integ/release_ci_test.py integ/release_platform_bundle_test.py From 3be80df18150e487c911edb5376b156ed4458167 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 04:12:16 -0700 Subject: [PATCH 05/12] FEATURE-ci-v3-runtime-green: tolerate borrowed-handle closure and enforce drain deadlines --- ChangeLog | 4 ++ docs/external-loop-contract.md | 9 +++++ examples/host_poll.hpp | 7 ++++ specs/tasks/FEATURE-ci-v3-runtime-green.md | 11 +++++ src/detail/connection_engine_websocket.cpp | 8 +++- test/unit/connection_engine_test.cpp | 47 +++++++++++++++++++++- 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index bd7c15dc2..e14f55724 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,8 @@ Version 3.0.0 - 2026-10-10 + Keep the HTTP/1 connection watchdog armed at a pending server drain + deadline so a stalled close handoff cannot outlive the drain budget. + Tolerate a concurrently closed borrowed registration in the Windows + reference host poll helper instead of failing the whole WSAPoll set. Register each native executable once in Automake test evidence inventories. Run Windows allocation-failure injection against the installed static library and require ordinary readiness cases in both linkage lanes. diff --git a/docs/external-loop-contract.md b/docs/external-loop-contract.md index c2877ded7..f193e039b 100644 --- a/docs/external-loop-contract.md +++ b/docs/external-loop-contract.md @@ -121,6 +121,15 @@ the resulting protocol/transport outcome. Duplicate or spurious notifications, including events with all flags false, are permitted and do not imply additional completions. Empty batches are permitted for timers and pending library work. +A registration borrowed by a snapshot can be closed by library work at any +moment, because retaining a snapshot never extends a handle lifetime. A POSIX +host observes this as a per-entry `POLLNVAL` readiness (translated to `error` +and ignored as stale). Windows `WSAPoll` instead rejects the entire call with +`WSAENOTSOCK`; a host must treat that failure like an empty batch — dispatch, +acquire a fresh snapshot, and wait again — rather than stopping, because the +closed registration is absent from every later snapshot. The reference +`examples/host_poll.hpp` helper implements this tolerance. + Obtain an initial snapshot before waiting. After **every successful dispatch**, obtain and reconcile a fresh snapshot before waiting again. A failed dispatch may have consumed part of a batch unless the documented rejection rule says diff --git a/examples/host_poll.hpp b/examples/host_poll.hpp index b864968d6..c30fbafaf 100644 --- a/examples/host_poll.hpp +++ b/examples/host_poll.hpp @@ -102,6 +102,13 @@ inline std::vector wait( const auto ready = ::WSAPoll(fds.data(), static_cast(fds.size()), timeout(deadline)); if (ready < 0) { const int error = ::WSAGetLastError(); + // Snapshot handles are borrowed: the library may close one between + // snapshot acquisition and this call. POSIX poll reports that + // per entry as POLLNVAL; WSAPoll rejects the whole set with + // WSAENOTSOCK. An empty batch keeps the contract's spurious- + // notification allowance: the caller dispatches, re-acquires + // interests, and the closed registration is already gone. + if (error == WSAENOTSOCK) return {}; if (error != WSAEINTR) throw std::runtime_error("WSAPoll failed (WSA error " + std::to_string(error) + ")"); } #else diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 326659289..18e2a0a1c 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -128,3 +128,14 @@ The duplicated names are http_cookie, consumer_v3_cookie, consumer_v3_cookie_wir Evidence: primary `.debug/v3-ci-native-repairs-PJFEkn/native-inventory/`. Autoreconf succeeded with existing AC_OUTPUT/.PHONY warnings. Initial config.status invocation used the source directory instead of its configured build directory and failed before any native execution; rechecking/regenerating in the correct build directory fixed setup. No unrelated source or generated tracked file changed. Current9c92 Windows package job122646921257 fails earlier at the public shared external consumer: exit6, captured stderr WSAPoll error10038. The selected820-byte process receipt was verified within a closed2,581,790-byte range assessment; no full artifact or original program ran. It contains no failed-call slot/state values. The earlier approved external4 capture remains CLOSED/PASS4/52/nonreproduction, and does not authorize running the distinct public consumer. No guessed polling or shutdown suppression was applied. A fresh consumer-specific bounded capture proposal is needed if further native diagnosis is required. + +## Stage 7: Windows WSAPoll borrowed-closure tolerance and HTTP/1 drain-deadline watchdog (2026-10-30) + +- [x] Correlate all retained Windows failures (external four-test suite twice, public shared consumer exit6) with the approved DIAG capture: the host loop dies on "WSAPoll failed (WSA error 10038)" while dispatch never failed and stopping was false. +- [x] Root-cause by contract analysis, not a new capture: `interest_snapshot` handles are borrowed and "retaining a snapshot does not extend any handle lifetime"; the library closes registrations under its registry mutex and never republishes dead records, so WSAENOTSOCK can only mean a snapshot handle closed between snapshot and WSAPoll entry (handle-value reuse makes it WSAENOTSOCK on busy runners and POLLNVAL on idle probes, reconciling the closed local probes). +- [x] Repair `examples/host_poll.hpp`: WSAENOTSOCK returns an empty batch (the contract's spurious-notification allowance); every reference caller re-acquires interests after each dispatch, and dead records are pruned from the next snapshot, so the loop provably progresses. All other WSAPoll failures still throw; POSIX POLLNVAL handling is unchanged. Document the host obligation in `docs/external-loop-contract.md`. +- [x] Root-cause the 2026-10-28 macOS/Darwin external4 drain stall (idle keep-alive connection never closed after `begin_drain`; host silent for the full 10s budget; remaining 1): `watchdog_terminal_locked()` treated the quiesce close mark as terminal, but HTTP/1 enforces the server drain deadline only through the watchdog (`enforce_timeout` is the sole HTTP/1 `scope_.expire_if_due` caller), so a stalled close handoff had no deadline enforcement left. +- [x] Repair: the close mark no longer retires the watchdog while a server drain deadline is pending; the deadline joins `plan_watchdog_locked` candidates through the existing `add_protocol_deadlines_locked` entry and fires through the unchanged global-expiry path. Deadline-less closes (request_close, response-Connection: close) stay terminal as before. +- [x] RED/GREEN regression `quiesce_with_drain_deadline_keeps_watchdog_armed` (unstarted engine, deterministic plan sample; deadline-less case pins unchanged terminal behavior) plus behavioral `quiesce_with_drain_deadline_still_closes_promptly`. RED verified against unfixed source, GREEN against the repair, five consecutive clean runs. +- [x] Linux focused gates: connection_engine 42/0 (x5), external_http1_e2e 4/0, websocket_drain 3/0, http1_websocket_upgrade 17/0, native_http1_e2e 62/0, external_readiness_adapter 28/0, examples/external_event_loop builds and serves; changed-file cpplint and `git diff --check` pass. +- [ ] Full exact-source CI on the working branch (mingw64/macOS/FreeBSD lanes remain nonlocal and deferred to CI). diff --git a/src/detail/connection_engine_websocket.cpp b/src/detail/connection_engine_websocket.cpp index dc0d79142..f805a7ef5 100644 --- a/src/detail/connection_engine_websocket.cpp +++ b/src/detail/connection_engine_websocket.cpp @@ -227,7 +227,13 @@ connection_engine::io_posture connection_engine::writer_posture( return io_posture::park; } bool connection_engine::watchdog_terminal_locked() const { - return shutdown_ || phase_ == stream_phase::terminal || (close_after_drain_ && !websocket_); + // The close mark alone must not retire the watchdog while a server + // drain deadline is pending: HTTP/1 enforces that deadline's hard + // stop only here (enforce_timeout's scope expiry), so a stalled + // close handoff may not outlive the drain budget. The transport's + // release still ends this loop with connection_closed. + return shutdown_ || phase_ == stream_phase::terminal + || (close_after_drain_ && !websocket_ && !server_drain_deadline_); } bool connection_engine::transport_output_pending_locked() const { if (outbox_.queued_bytes() > 0) return true; diff --git a/test/unit/connection_engine_test.cpp b/test/unit/connection_engine_test.cpp index 5eecea11b..13931924c 100644 --- a/test/unit/connection_engine_test.cpp +++ b/test/unit/connection_engine_test.cpp @@ -68,6 +68,7 @@ #include #include "./io_loopback.hpp" +#include "./websocket_engine_helpers.hpp" #include "./littletest.hpp" namespace { @@ -301,7 +302,7 @@ struct scenario { explicit scenario(srv::timeout_options timeouts_in) : scenario(srv::budget_limits(), timeouts_in) { } - bool start_engine() { + bool create_engine() { if (!pair.ok()) return false; backend.adopt_connection(kConnId, pair.detach_local()); engine = std::make_shared(backend, pool, registry, @@ -310,6 +311,11 @@ struct scenario { [this] { stopped.store(true); }); + return true; + } + + bool start_engine() { + if (!create_engine()) return false; engine->start(); return true; } @@ -1215,6 +1221,45 @@ LT_BEGIN_AUTO_TEST(connection_engine_suite, quiesce_idle_connection_closes_promp LT_CHECK(reaches_eof(s.pair.peer())); LT_END_AUTO_TEST(quiesce_idle_connection_closes_promptly) +// TASK-110 pin: a quiesce that carries a server drain deadline keeps the +// watchdog armed at that deadline. HTTP/1 enforces the drain budget's +// hard stop only through the watchdog (enforce_timeout's scope expiry), +// so the close mark alone must not retire it while the deadline is +// pending -- a stalled close handoff may not outlive the drain budget. +// Loops stay unstarted so the plan is sampled before any close path can +// complete; a deadline-less quiesce stays terminal exactly as before. +LT_BEGIN_AUTO_TEST(connection_engine_suite, quiesce_with_drain_deadline_keeps_watchdog_armed) + srv::timeout_options timeouts; + timeouts.header = std::chrono::milliseconds(10000); + scenario s(timeouts); + LT_CHECK(s.create_engine()); + const auto drain_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + s.engine->quiesce(drain_deadline); + const auto armed = httpserver::detail::connection_engine_test_access::deadline(*s.engine); + LT_CHECK(armed.has_value()); + if (armed) LT_CHECK(*armed <= drain_deadline); + scenario plain; + LT_CHECK(plain.create_engine()); + plain.engine->quiesce(); + LT_CHECK(!httpserver::detail::connection_engine_test_access::deadline(*plain.engine).has_value()); +LT_END_AUTO_TEST(quiesce_with_drain_deadline_keeps_watchdog_armed) + +// The armed drain deadline must not delay the ordinary close chain: a +// started engine quiesced with a far drain budget still ends (and the +// transport closes) promptly, as in the deadline-less case above. +LT_BEGIN_AUTO_TEST(connection_engine_suite, quiesce_with_drain_deadline_still_closes_promptly) + srv::timeout_options timeouts; + timeouts.header = std::chrono::milliseconds(10000); + scenario s(timeouts); + LT_CHECK(s.start_engine()); + const auto began = std::chrono::steady_clock::now(); + s.engine->quiesce(std::chrono::steady_clock::now() + std::chrono::seconds(10)); + LT_CHECK(wait_until([&s] { return s.stopped.load(); })); + const auto elapsed = std::chrono::steady_clock::now() - began; + LT_CHECK(elapsed < std::chrono::milliseconds(5000)); + LT_CHECK(reaches_eof(s.pair.peer())); +LT_END_AUTO_TEST(quiesce_with_drain_deadline_still_closes_promptly) + // TASK-110: the counted units bracket the exchange -- the handler runs // between the connection unit and its own exchange unit (2 live), and // the count returns to zero once the engine finalized. From 21f37bad6c55052c3fb9a1ae18c9af394ad0ea9d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 04:21:40 -0700 Subject: [PATCH 06/12] FEATURE-ci-v3-runtime-green: raise FreeBSD job timeout to observed workload --- .github/workflows/v3-package-validation.yml | 2 +- .github/workflows/v3-release-acceptance.yml | 2 +- specs/tasks/FEATURE-ci-v3-runtime-green.md | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/v3-package-validation.yml b/.github/workflows/v3-package-validation.yml index 6b3f3c3bc..dd4b1bce0 100644 --- a/.github/workflows/v3-package-validation.yml +++ b/.github/workflows/v3-package-validation.yml @@ -149,7 +149,7 @@ jobs: if-no-files-found: error freebsd: runs-on: ubuntu-24.04 - timeout-minutes: 180 + timeout-minutes: 240 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: freebsd_instance diff --git a/.github/workflows/v3-release-acceptance.yml b/.github/workflows/v3-release-acceptance.yml index e2fc38dd3..57d0cd6c3 100644 --- a/.github/workflows/v3-release-acceptance.yml +++ b/.github/workflows/v3-release-acceptance.yml @@ -269,7 +269,7 @@ jobs: freebsd: runs-on: ubuntu-24.04 needs: prepare - timeout-minutes: 180 + timeout-minutes: 240 outputs: artifact_id: ${{ steps.proof.outputs.artifact-id }} artifact_digest: ${{ steps.proof.outputs.artifact-digest }} diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 18e2a0a1c..b153f8a50 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -139,3 +139,11 @@ Current9c92 Windows package job122646921257 fails earlier at the public shared e - [x] RED/GREEN regression `quiesce_with_drain_deadline_keeps_watchdog_armed` (unstarted engine, deterministic plan sample; deadline-less case pins unchanged terminal behavior) plus behavioral `quiesce_with_drain_deadline_still_closes_promptly`. RED verified against unfixed source, GREEN against the repair, five consecutive clean runs. - [x] Linux focused gates: connection_engine 42/0 (x5), external_http1_e2e 4/0, websocket_drain 3/0, http1_websocket_upgrade 17/0, native_http1_e2e 62/0, external_readiness_adapter 28/0, examples/external_event_loop builds and serves; changed-file cpplint and `git diff --check` pass. - [ ] Full exact-source CI on the working branch (mingw64/macOS/FreeBSD lanes remain nonlocal and deferred to CI). + +## Stage 8: FreeBSD job timeout (2026-10-30) + +- [x] Evidence: the 2026-10-26 acceptance FreeBSD job completed its native proof at 2h59m45s (57 seconds under the 180-minute job timeout); the 2026-10-28 package-validation FreeBSD job was cancelled by the 180-minute timeout at 3h3m mid-run. The workload (libc++ 20.1.8 runtime build + provider + both TLS lanes on a 2-CPU VM) legitimately straddles the limit. +- [x] Raise only the freebsd job `timeout-minutes` from 180 to 240 in both `v3-package-validation.yml` and `v3-release-acceptance.yml`; every other job keeps its existing timeout. `test-v3-package-workflow.py` (4/4) and `test-v3-ci-workflows.py` (31/31) pass. +- [ ] Full FreeBSD CI evidence with the corrected budget. + +The 2026-10-26 FreeBSD tls-yes `check-v3-public-http2.py` shared-external failure ("no retired connection generation captured", one occurrence) is retained as an open flake: the consumer's `stale` check depends on the single-point `snapshot` having captured the soon-to-retire H2 connection's published registration, and the saved pair in that run was still live at `stale` time. No consumer change is made without a recurrence; the 2026-10-30 runs will decide. From fbf072eb7ba3eea35d8f278e78dd6f82c4ad72a1 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 04:56:25 -0700 Subject: [PATCH 07/12] FEATURE-ci-v3-runtime-green: record local TLS=no baseline observation --- specs/tasks/FEATURE-ci-v3-runtime-green.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index b153f8a50..6fa54741b 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -147,3 +147,5 @@ Current9c92 Windows package job122646921257 fails earlier at the public shared e - [ ] Full FreeBSD CI evidence with the corrected budget. The 2026-10-26 FreeBSD tls-yes `check-v3-public-http2.py` shared-external failure ("no retired connection generation captured", one occurrence) is retained as an open flake: the consumer's `stale` check depends on the single-point `snapshot` having captured the soon-to-retire H2 connection's published registration, and the saved pair in that run was still live at `stale` time. No consumer change is made without a recurrence; the 2026-10-30 runs will decide. + +Full local `make check -j1` (TLS=no, g++ 15.2, WSL2): 232/235 pass, 2 platform skips (iocp/kqueue), 1 FAIL `quic_flow_control` (12 checks expecting ceiling-rejection throws). Byte-identical failures with stage 7 reverted and at b71192c9, so pre-existing and unrelated to this repair; CI builds this test with clang where it passes. Recorded as a local g++ environment observation, not a regression. From b8edaebadea0126410bab1af815468d33d700adc Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 05:28:50 -0700 Subject: [PATCH 08/12] FEATURE-ci-v3-runtime-green: allow the version-info system import in Windows audits --- ChangeLog | 2 ++ scripts/audit-v3-installed-package.py | 3 ++- specs/tasks/FEATURE-ci-v3-runtime-green.md | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index e14f55724..d9a2db1d3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ Version 3.0.0 - 2026-10-10 + Resolve the Windows version-information system import in installed + package dependency audits. Keep the HTTP/1 connection watchdog armed at a pending server drain deadline so a stalled close handoff cannot outlive the drain budget. Tolerate a concurrently closed borrowed registration in the Windows diff --git a/scripts/audit-v3-installed-package.py b/scripts/audit-v3-installed-package.py index 260536f17..c2e5ea153 100644 --- a/scripts/audit-v3-installed-package.py +++ b/scripts/audit-v3-installed-package.py @@ -139,7 +139,8 @@ def parse_otool(output, artifact): WINDOWS_SYSTEM = {'kernel32.dll', 'kernelbase.dll', 'ntdll.dll', 'msvcrt.dll', 'ucrtbase.dll', 'ws2_32.dll', 'advapi32.dll', 'bcrypt.dll', 'bcryptprimitives.dll', 'crypt32.dll', 'secur32.dll', 'user32.dll', 'iphlpapi.dll', 'winmm.dll', 'ole32.dll', 'shell32.dll', - 'userenv.dll', 'ncrypt.dll', 'gdi32.dll', 'shlwapi.dll', 'comdlg32.dll'} + 'userenv.dll', 'ncrypt.dll', 'gdi32.dll', 'shlwapi.dll', 'comdlg32.dll', + 'version.dll'} def windows_system(name): diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 6fa54741b..c2c01cd0f 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -149,3 +149,10 @@ Current9c92 Windows package job122646921257 fails earlier at the public shared e The 2026-10-26 FreeBSD tls-yes `check-v3-public-http2.py` shared-external failure ("no retired connection generation captured", one occurrence) is retained as an open flake: the consumer's `stale` check depends on the single-point `snapshot` having captured the soon-to-retire H2 connection's published registration, and the saved pair in that run was still live at `stale` time. No consumer change is made without a recurrence; the 2026-10-30 runs will decide. Full local `make check -j1` (TLS=no, g++ 15.2, WSL2): 232/235 pass, 2 platform skips (iocp/kqueue), 1 FAIL `quic_flow_control` (12 checks expecting ceiling-rejection throws). Byte-identical failures with stage 7 reverted and at b71192c9, so pre-existing and unrelated to this repair; CI builds this test with clang where it passes. Recorded as a local g++ environment observation, not a regression. + +## Stage 9: Windows audit client closure (2026-10-30) + +- [x] The 2026-10-30 mingw64 package run (job c8dc2d64) passed every external/installed suite in both linkages for the first time (stage 7 repair holding) and advanced to `check-v3-public-tls.py`, which failed walking the Python TLS client: `AuditError: unresolved PE import: version.dll` from `_ssl.pyd` -> `libpython3.14.dll` (its import table lists VERSION.dll; api-ms-win-* names already match the existing pattern rule). +- [x] Verified against the real MSYS2 mingw64 packages (python 3.14.9-1, openssl 3.6.5-1, libgcc 16.2.0-6, winpthreads, libstdc++): after adding `version.dll` to `WINDOWS_SYSTEM`, the full `_ssl` client closure resolves -- remaining names are WINDOWS_SYSTEM entries or MSYS2 runtime DLLs that the same run's own recorded suite graphs already resolve through `V3_PACKAGE_DLL_DIRS`. The mingw-built integration closures (from the run's suites.json) import only allowlisted system DLLs. +- [x] One-line allowlist addition in `scripts/audit-v3-installed-package.py`; `version.dll` is a documented System32 component (version-information APIs). `test-v3-installed-package.py` 60/60; `test-v3-installed-consumer.py` 97/97 (one unrelated compile-timing flake on the first local run, clean on rerun). +- [ ] Full mingw64 CI evidence past the client audit. From 021a00c37d78f81c81b06a39b4697880d859639a Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 10:26:08 -0700 Subject: [PATCH 09/12] FEATURE-ci-v3-runtime-green: link Windows public audit clients to Winsock --- ChangeLog | 2 ++ scripts/check-v3-public-http2.py | 13 ++++++++++--- scripts/check-v3-public-tls.py | 10 +++++++--- specs/tasks/FEATURE-ci-v3-runtime-green.md | 7 +++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index d9a2db1d3..773d81306 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ Version 3.0.0 - 2026-10-10 + Link the installed public TLS and HTTP/2 audit clients to Winsock on + Windows and name their executables with the platform suffix. Resolve the Windows version-information system import in installed package dependency audits. Keep the HTTP/1 connection watchdog armed at a pending server drain diff --git a/scripts/check-v3-public-http2.py b/scripts/check-v3-public-http2.py index 2a186e258..38af737e3 100755 --- a/scripts/check-v3-public-http2.py +++ b/scripts/check-v3-public-http2.py @@ -146,8 +146,15 @@ def run(args): flags = ['--libs'] + (['--static'] if linkage == 'static' else []) libraries = shlex.split(command(['pkg-config'] + flags + ['libhttpserver'], consumer, env)) if linkage == 'static': libraries = [str(prefix / 'lib/libhttpserver.a') if f == '-lhttpserver' else f for f in libraries] - libraries += ['-pthread', '-Wl,-rpath,' + str(prefix / 'lib'), '-Wl,-rpath,' + str(provider / 'lib')] + shlex.split(args.sanitize) - executable = consumer / linkage + # Winsock is a system library on Windows and lives in libc on the + # POSIX targets, whose runtime search needs the explicit rpath. + if os.name == 'nt': + libraries += ['-lws2_32', '-lmswsock', '-lbcrypt'] + else: + libraries += ['-pthread', '-Wl,-rpath,' + str(prefix / 'lib'), '-Wl,-rpath,' + str(provider / 'lib')] + libraries += shlex.split(args.sanitize) + suffix = '.exe' if os.name == 'nt' else '' + executable = consumer / (linkage + suffix) provenance = compile_probe(args.compiler, [consumer / 'http2_consumer.cpp'], cflags + shlex.split(args.sanitize), libraries, executable, command, env, source, build, prefix, False) validate_public(True, provenance['dependencies'], prefix, source, build) @@ -155,7 +162,7 @@ def run(args): violations = audit.graph_violations(graph, 'strict', args.tls, 'aggregate', audit.load_policy(), provider) require(not violations, 'dependency audit: ' + repr(violations)) if args.tls == 'yes': - example = consumer / (linkage + '-example') + example = consumer / (linkage + '-example' + suffix) example_compile = compile_probe(args.compiler, [consumer / 'http2.cpp'], cflags + shlex.split(args.sanitize), libraries, example, command, env, source, build, prefix, False) example_graph = audit.dependency_graph([str(example)]); check_linkage(example_graph, linkage, prefix) diff --git a/scripts/check-v3-public-tls.py b/scripts/check-v3-public-tls.py index b356db3dc..1dd0d8d21 100755 --- a/scripts/check-v3-public-tls.py +++ b/scripts/check-v3-public-tls.py @@ -330,17 +330,21 @@ def run(args): if identity == 'client-valid': chain += (fixtures / 'client-intermediate.pem').read_bytes() (fixtures / (identity + '-chain.pem')).write_bytes(chain) - independent = consumer / 'independent-client' + independent = consumer / ('independent-client' + ('.exe' if os.name == 'nt' else '')) + # tls_client.cpp dials raw sockets: Winsock is a system library on + # Windows and lives in libc on the POSIX targets. + independent_system = ['-lws2_32'] if os.name == 'nt' else [] command_log([args.compiler, '-std=c++20', '-I' + str(args.provider_prefix / 'include'), consumer / 'tls_client.cpp', '-L' + str(args.provider_prefix / 'lib'), - '-Wl,-rpath,' + str(args.provider_prefix / 'lib'), '-lssl', '-lcrypto', '-o', independent], consumer, env) + '-Wl,-rpath,' + str(args.provider_prefix / 'lib'), '-lssl', '-lcrypto'] + + independent_system + ['-o', independent], consumer, env) receipt['independent_client'] = dict(executable=str(independent), sha256=hashlib.sha256(independent.read_bytes()).hexdigest(), dependencies=audit.dependency_graph([str(independent)])) check('libhttpserver' not in json.dumps(receipt['independent_client']['dependencies']), 'client links server library') command_log([independent, 'fixtures', fixtures], consumer, env, timeout=10) (fixtures / 'malformed.pem').write_text('malformed certificate') (fixtures / 'malformed-key.pem').write_text('malformed private key') - command_log([args.provider_prefix / 'bin/openssl', 'pkey', '-in', fixtures / 'acme-key.pem', '-aes-256-cbc', '-passout', 'pass:task194-test-only', '-out', fixtures / 'encrypted-key.pem'], consumer, env) + command_log([args.provider_prefix / 'bin' / ('openssl.exe' if os.name == 'nt' else 'openssl'), 'pkey', '-in', fixtures / 'acme-key.pem', '-aes-256-cbc', '-passout', 'pass:task194-test-only', '-out', fixtures / 'encrypted-key.pem'], consumer, env) (fixtures / 'extra.pem').write_bytes((fixtures / 'acme.pem').read_bytes() * 2) for name in ('mismatch-key', 'past-expiry', 'late-expiry'): shutil.copyfile(fixtures / 'acme.pem', fixtures / (name + '.pem')) diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index c2c01cd0f..a4d7c9b04 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -156,3 +156,10 @@ Full local `make check -j1` (TLS=no, g++ 15.2, WSL2): 232/235 pass, 2 platform s - [x] Verified against the real MSYS2 mingw64 packages (python 3.14.9-1, openssl 3.6.5-1, libgcc 16.2.0-6, winpthreads, libstdc++): after adding `version.dll` to `WINDOWS_SYSTEM`, the full `_ssl` client closure resolves -- remaining names are WINDOWS_SYSTEM entries or MSYS2 runtime DLLs that the same run's own recorded suite graphs already resolve through `V3_PACKAGE_DLL_DIRS`. The mingw-built integration closures (from the run's suites.json) import only allowlisted system DLLs. - [x] One-line allowlist addition in `scripts/audit-v3-installed-package.py`; `version.dll` is a documented System32 component (version-information APIs). `test-v3-installed-package.py` 60/60; `test-v3-installed-consumer.py` 97/97 (one unrelated compile-timing flake on the first local run, clean on rerun). - [ ] Full mingw64 CI evidence past the client audit. + +## Stage 10: Windows public audit client linkage (2026-10-30) + +- [x] Evidence: the 2026-10-30 mingw64 package job (124062430922) and the Windows acceptance job (124062636691) both advanced past every earlier gate (installed suites, traffic cells, compiled operations and the Python TLS client audit of stage 9) and failed at the same place: linking the independent TLS client in `check-v3-public-tls.py`, with `undefined reference to __imp_WSAStartup/__imp_socket/__imp_htons/__imp_htonl/__imp_connect/__imp_closesocket` (tls_client.cpp already carries its own `_WIN32` Winsock path; only the link line lacked the system library). +- [x] Repair in `scripts/check-v3-public-tls.py`: the independent client is named with the platform `.exe` suffix and linked with `-lws2_32` on `os.name == 'nt'` (POSIX targets unchanged), and the provider `openssl` tool is invoked with its explicit `.exe` name on Windows, matching `run-v3-release-ci.py`. +- [x] Same-class parity repair in `scripts/check-v3-public-http2.py` (preemptive, code-audit driven, not yet exercised on Windows): consumers and examples get the `.exe` suffix and the `ws2_32/mswsock/bcrypt` system libraries replace the POSIX-only `-pthread`/`-rpath` line, matching the `check-v3-installed-consumer.py`/`check-v3-public-tls.py` convention; runtime DLL resolution stays on the `runtime_environment` PATH/V3_PACKAGE_DLL_DIRS mechanism. Local gates: `test-v3-public-http2.py` 6/6, `py_compile` on both scripts. +- [ ] Full mingw64/Windows CI evidence past the public TLS client link. From b080f7446c1e41c3649ab251b614a10fa3ea5e5e Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 11:13:50 -0700 Subject: [PATCH 10/12] FEATURE-ci-v3-runtime-green: complete first FreeBSD native make-check exposure --- .github/workflows/v3-release-acceptance.yml | 8 ++++++- ChangeLog | 3 +++ scripts/run-v3-http3-full-gates.py | 7 ++++-- specs/tasks/FEATURE-ci-v3-runtime-green.md | 10 +++++++++ test/Makefile.am | 6 ++++++ test/integ/native_protocol_memory_test.cpp | 24 +++++++++++++++++++++ test/timeout-wrapper.sh | 8 +++++++ 7 files changed, 63 insertions(+), 3 deletions(-) create mode 100755 test/timeout-wrapper.sh diff --git a/.github/workflows/v3-release-acceptance.yml b/.github/workflows/v3-release-acceptance.yml index 57d0cd6c3..8089ecc5f 100644 --- a/.github/workflows/v3-release-acceptance.yml +++ b/.github/workflows/v3-release-acceptance.yml @@ -269,7 +269,7 @@ jobs: freebsd: runs-on: ubuntu-24.04 needs: prepare - timeout-minutes: 240 + timeout-minutes: 300 outputs: artifact_id: ${{ steps.proof.outputs.artifact-id }} artifact_digest: ${{ steps.proof.outputs.artifact-digest }} @@ -332,6 +332,12 @@ jobs: fi test "$(node --version)" = v24.15.0 python3 -c 'import subprocess; subprocess.run(["git","--version"],check=True)' + # The native peer-policy runtime test binds 127.0.0.2 as its + # admin source address; FreeBSD only configures 127.0.0.1 on + # lo0 by default, so the alias must exist before make check. + if ifconfig lo0 >/dev/null 2>&1; then + ifconfig lo0 alias 127.0.0.2 2>/dev/null || ifconfig lo0 | grep -q '127\.0\.0\.2' + fi python3 scripts/ci-freebsd-toolchain.py --output "$RUNNER_TEMP/platform-evidence/toolchain" . "$RUNNER_TEMP/platform-evidence/toolchain/toolchain.env" python3 scripts/prepare-v3-package-ci.py --output "$RUNNER_TEMP/prerequisites" --make gmake diff --git a/ChangeLog b/ChangeLog index 773d81306..568a94104 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,7 @@ Version 3.0.0 - 2026-10-10 + Complete the first FreeBSD native make-check exposure: portable + process-memory measurement, GNU-make gate selection, the + loopback admin alias, per-test hang bounds and job budget. Link the installed public TLS and HTTP/2 audit clients to Winsock on Windows and name their executables with the platform suffix. Resolve the Windows version-information system import in installed diff --git a/scripts/run-v3-http3-full-gates.py b/scripts/run-v3-http3-full-gates.py index 3d9fb1fb4..44c22235b 100644 --- a/scripts/run-v3-http3-full-gates.py +++ b/scripts/run-v3-http3-full-gates.py @@ -20,6 +20,9 @@ ROOT = Path(__file__).resolve().parents[1] INVENTORY = ROOT / 'test/conformance/http3/full-gates.json' +# Generated Makefiles need GNU make; FreeBSD's default `make` is BSD +# make, so prefer gmake where it exists and fall back to `make`. +MAKE = shutil.which('gmake') or 'make' OFF_SUITES = '''http3_frame http3_settings http3_connection http3_headers http3_exchange http3_streaming http3_cancellation http3_qpack http3_qpack_scheduling http3_drain http3_fuzz_replay qpack_primitives qpack_field_section qpack_dynamic_table @@ -374,10 +377,10 @@ def build_configuration(build, tls, sanitized): def native_stage(build, suites, directory, fixtures=False): directory.mkdir(mode=0o700) - commands = [checked(['make', '-j4', '-C', build / 'src'], ROOT, directory / 'build')] + commands = [checked([MAKE, '-j4', '-C', build / 'src'], ROOT, directory / 'build')] targets = suites + (['http3_udp_fixture', 'http3_semantic_parity_fixture', 'quic_interop_endpoint'] if fixtures else []) selection = [name + '=' + ' '.join(targets) for name in ('TESTS', 'check_PROGRAMS', 'noinst_PROGRAMS')] - commands.append(checked(['make', '-j4', '-C', build / 'test', *selection, *targets], ROOT, directory / 'test-build')) + commands.append(checked([MAKE, '-j4', '-C', build / 'test', *selection, *targets], ROOT, directory / 'test-build')) results = run_native_suites(build, suites, directory / 'tests') results['commands'] = commands tls = '#define NATIVE_V3_TLS' in (build / 'config.h').read_text() diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index a4d7c9b04..0960e8f6a 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -163,3 +163,13 @@ Full local `make check -j1` (TLS=no, g++ 15.2, WSL2): 232/235 pass, 2 platform s - [x] Repair in `scripts/check-v3-public-tls.py`: the independent client is named with the platform `.exe` suffix and linked with `-lws2_32` on `os.name == 'nt'` (POSIX targets unchanged), and the provider `openssl` tool is invoked with its explicit `.exe` name on Windows, matching `run-v3-release-ci.py`. - [x] Same-class parity repair in `scripts/check-v3-public-http2.py` (preemptive, code-audit driven, not yet exercised on Windows): consumers and examples get the `.exe` suffix and the `ws2_32/mswsock/bcrypt` system libraries replace the POSIX-only `-pthread`/`-rpath` line, matching the `check-v3-installed-consumer.py`/`check-v3-public-tls.py` convention; runtime DLL resolution stays on the `runtime_environment` PATH/V3_PACKAGE_DLL_DIRS mechanism. Local gates: `test-v3-public-http2.py` 6/6, `py_compile` on both scripts. - [ ] Full mingw64/Windows CI evidence past the public TLS client link. + +## Stage 11: first FreeBSD native make-check exposure (2026-10-30) + +- [x] Evidence (5.1GB failure artifact 12993618134, run 41055662147, analyzed in full): the FreeBSD acceptance lane passed the entire installed matrix (both TLS lanes incl. public-tls and public-http2) in ~18 minutes, then reached `gmake check` for the first time ever (every earlier acceptance run died earlier: the Oct-26 runs at the http2 stale flake/copyback, the Oct-28 runs at the 180-min timeout). The first `make check` ran the full 7200s command deadline and was killed with only `response_sources` still unfinished: test 24 `threaded_pipe_sends_single_winner` started and never printed a result. Four real test failures were also recorded: `response_definition` (checks 846/847, `file_send_truncated_mid_send_fails`, completed in 2.58ms with `done==0`), `native_protocol_memory` ("process memory measurement unavailable" — no FreeBSD branch), `consumer_v3_peer_policy_runtime` (line 86 = bind/connect of the 127.0.0.2 admin source: FreeBSD has no 127/8 loopback alias by default), and `integ/http3_full_gate_runner_test.py` (`make -j4` in the gate runner is BSD make on FreeBSD; needs gmake). +- [x] Repairs with proven causes: `native_protocol_memory_test.cpp` gains FreeBSD (kinfo_proc sysctl) and Windows (GetProcessMemoryInfo) resident-memory branches — the Windows acceptance make check needs the same branch; `run-v3-http3-full-gates.py` selects gmake when present (GNU-make Makefiles, BSD default make); the freebsd acceptance VM run script adds-or-verifies the `lo0` 127.0.0.2 alias (no-op where `ifconfig lo0` is absent, e.g. the Linux workflow tests). +- [x] Hang bound: `test/timeout-wrapper.sh` (EXTRA_DIST, `LOG_COMPILER`) wraps every native test with `timeout -k 60 600` where timeout(1) exists, so a recurrence cannot consume a whole CI make-check budget; hosts without timeout (macOS default) run exactly as before. +- [x] Budget: freebsd acceptance `timeout-minutes` 240 → 300 (observed: ~95 min prepare with the pinned-Node source build + ~18 min matrix + make check ×2 + evidence recovery). +- [x] Local gates: fresh worktree configure/build; `native_protocol_memory` passes under the new LOG_COMPILER wrapper; `test-v3-ci-workflows.py` 31/31, `test-v3-platform-ci.py` 20/20, `test-v3-http3-full-gates.py` 1/1; changed-file cpplint and YAML parse pass. +- [ ] Open flakes (no repair without a recurrence, per the standing http2-stale precedent): `response_sources` test 24 hang (one occurrence; the claim/close guard is a single atomic CAS and `pull_handle`'s fread cannot block on a pipe whose write end is closed — mechanism unexplained) and `response_definition` checks 846/847 (coroutine did not complete after the mid-send truncation). Both pass on Linux/macOS; the next FreeBSD run with bounded hangs decides. +- [ ] Full FreeBSD acceptance CI evidence. diff --git a/test/Makefile.am b/test/Makefile.am index d0daa7085..c42628fd5 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -727,6 +727,12 @@ consumer_v3_features_CPPFLAGS = -I$(top_srcdir)/src consumer_v3_features_LDADD = $(top_builddir)/src/libhttpserver_v3core.la AM_TESTS_ENVIRONMENT = V3_TLS_MODE="$(enable_v3_tls)"; export V3_TLS_MODE; +# Bound every native test program so one hung binary cannot consume a +# whole CI make-check command budget; the wrapper degrades to a plain +# exec where timeout(1) is unavailable (macOS by default). +LOG_COMPILER = $(top_srcdir)/test/timeout-wrapper.sh +EXTRA_DIST += timeout-wrapper.sh + basic_auth_corpus_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) body_reader_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) diff --git a/test/integ/native_protocol_memory_test.cpp b/test/integ/native_protocol_memory_test.cpp index d799643aa..0f7223595 100644 --- a/test/integ/native_protocol_memory_test.cpp +++ b/test/integ/native_protocol_memory_test.cpp @@ -21,6 +21,14 @@ #if defined(__APPLE__) #include +#elif defined(__FreeBSD__) +#include +#include +#include +#include +#elif defined(_WIN32) +#include +#include #else #include #include @@ -46,6 +54,22 @@ std::size_t resident_bytes() { check(task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast(&info), &count) == KERN_SUCCESS, "process memory measurement unavailable"); return info.resident_size; +#elif defined(__FreeBSD__) + // FreeBSD ships no procfs by default; the native source is the + // per-process sysctl MIB (kinfo_proc's resident page count). + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, static_cast(::getpid())}; + kinfo_proc info{}; + size_t size = sizeof info; + check(::sysctl(mib, 4, &info, &size, nullptr, 0) == 0 + && size >= offsetof(kinfo_proc, ki_rssize) + sizeof info.ki_rssize, + "process memory measurement unavailable"); + return static_cast(info.ki_rssize) + * static_cast(::sysconf(_SC_PAGESIZE)); +#elif defined(_WIN32) + PROCESS_MEMORY_COUNTERS counters{}; + check(::GetProcessMemoryInfo(::GetCurrentProcess(), &counters, sizeof counters) != 0, + "process memory measurement unavailable"); + return counters.WorkingSetSize; #else std::ifstream stat("/proc/self/statm"); std::size_t total = 0, resident = 0; diff --git a/test/timeout-wrapper.sh b/test/timeout-wrapper.sh new file mode 100755 index 000000000..7b962efcf --- /dev/null +++ b/test/timeout-wrapper.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Bound one native test program so a single hung binary cannot consume +# a whole CI make-check command budget. Hosts without timeout(1) +# (macOS by default) run tests exactly as before. +if command -v timeout >/dev/null 2>&1; then + exec timeout -k 60 600 "$@" +fi +exec "$@" From db7cefecb6b5858872d9a8cd090134e3536d3658 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 12:08:50 -0700 Subject: [PATCH 11/12] FEATURE-ci-v3-runtime-green: open interop keylog with Windows-compatible flags --- ChangeLog | 2 ++ specs/tasks/FEATURE-ci-v3-runtime-green.md | 6 ++++++ src/detail/tls_session.cpp | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/ChangeLog b/ChangeLog index 568a94104..2608a1363 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ Version 3.0.0 - 2026-10-10 + Open the QUIC interop test keylog with Windows-compatible flags + instead of O_NOFOLLOW/O_CLOEXEC/fchmod. Complete the first FreeBSD native make-check exposure: portable process-memory measurement, GNU-make gate selection, the loopback admin alias, per-test hang bounds and job budget. diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 0960e8f6a..85c01a8af 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -173,3 +173,9 @@ Full local `make check -j1` (TLS=no, g++ 15.2, WSL2): 232/235 pass, 2 platform s - [x] Local gates: fresh worktree configure/build; `native_protocol_memory` passes under the new LOG_COMPILER wrapper; `test-v3-ci-workflows.py` 31/31, `test-v3-platform-ci.py` 20/20, `test-v3-http3-full-gates.py` 1/1; changed-file cpplint and YAML parse pass. - [ ] Open flakes (no repair without a recurrence, per the standing http2-stale precedent): `response_sources` test 24 hang (one occurrence; the claim/close guard is a single atomic CAS and `pull_handle`'s fread cannot block on a pipe whose write end is closed — mechanism unexplained) and `response_definition` checks 846/847 (coroutine did not complete after the mid-send truncation). Both pass on Linux/macOS; the next FreeBSD run with bounded hangs decides. - [ ] Full FreeBSD acceptance CI evidence. + +## Stage 12: Windows TLS-on interop keylog portability (2026-10-30) + +- [x] Evidence (mingw64 job 124232738670, Windows acceptance job 124233097485, both at b080f744): with stage 10 in place the mingw64 package lane printed `PASS installed consumer native TLS=no` — the entire installed tls-no matrix including public-tls and public-http2 passed on Windows for the first time. The tls-yes lane then failed building `libquic_interop_provider_a`: `src/detail/tls_session.cpp:127/129` use `O_NOFOLLOW`, `O_CLOEXEC` and `fchmod`, none of which exist on Windows. +- [x] Repair: `open_keylog()` gains a `_WIN32` branch using `_O_NOINHERIT` (the close-on-exec analog) and dropping `fchmod` (the creating open already owns the file); the POSIX branch is unchanged. This is the only site in `src/` using those identifiers (`quic_tls_session.cpp` is clean). POSIX-branch syntax check with the interop define passes locally; cpplint clean. +- [ ] Full Windows tls-yes CI evidence past the interop provider build (SNI/rotation scenarios and the tls-yes public audits remain first-exposure). diff --git a/src/detail/tls_session.cpp b/src/detail/tls_session.cpp index baa20cc9e..9de2cbc7b 100644 --- a/src/detail/tls_session.cpp +++ b/src/detail/tls_session.cpp @@ -124,11 +124,21 @@ struct tls_session::impl { void open_keylog() { const char* path = std::getenv("SSLKEYLOGFILE"); if (!quic || !interop_mode() || !path || !*path) return; +#if defined(_WIN32) + // Windows has no O_NOFOLLOW/O_CLOEXEC/fchmod; _O_NOINHERIT is the + // close-on-exec analog and the creating open already owns the file. + keylog_fd = ::open(path, O_WRONLY | O_CREAT | O_APPEND | _O_NOINHERIT, 0600); + struct stat info{}; + if (keylog_fd < 0 || fstat(keylog_fd, &info) != 0 || !S_ISREG(info.st_mode)) { + throw std::runtime_error("test keylog unavailable"); + } +#else keylog_fd = ::open(path, O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW | O_CLOEXEC, 0600); struct stat info{}; if (keylog_fd < 0 || fstat(keylog_fd, &info) != 0 || !S_ISREG(info.st_mode) || fchmod(keylog_fd, 0600) != 0) { throw std::runtime_error("test keylog unavailable"); } +#endif } static void keylog(const SSL* ssl, const char* line) noexcept { auto* self = state(const_cast(ssl)); From 5b2db29acdc16f97c7c684353d58e0b184c0457f Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Fri, 30 Oct 2026 13:04:45 -0700 Subject: [PATCH 12/12] FEATURE-ci-v3-runtime-green: poll QUIC fixtures through the host helper on Windows --- ChangeLog | 2 ++ specs/tasks/FEATURE-ci-v3-runtime-green.md | 7 ++++++ test/integ/http3_udp_fixture.cpp | 21 +++++++++++++---- test/integ/quic_interop_endpoint.cpp | 26 ++++++---------------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/ChangeLog b/ChangeLog index 2608a1363..870b18921 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,6 @@ Version 3.0.0 - 2026-10-10 + Poll the QUIC interop endpoint and UDP fixture through the shared + cross-platform host helper on Windows. Open the QUIC interop test keylog with Windows-compatible flags instead of O_NOFOLLOW/O_CLOEXEC/fchmod. Complete the first FreeBSD native make-check exposure: portable diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 85c01a8af..a7d5b0f73 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -179,3 +179,10 @@ Full local `make check -j1` (TLS=no, g++ 15.2, WSL2): 232/235 pass, 2 platform s - [x] Evidence (mingw64 job 124232738670, Windows acceptance job 124233097485, both at b080f744): with stage 10 in place the mingw64 package lane printed `PASS installed consumer native TLS=no` — the entire installed tls-no matrix including public-tls and public-http2 passed on Windows for the first time. The tls-yes lane then failed building `libquic_interop_provider_a`: `src/detail/tls_session.cpp:127/129` use `O_NOFOLLOW`, `O_CLOEXEC` and `fchmod`, none of which exist on Windows. - [x] Repair: `open_keylog()` gains a `_WIN32` branch using `_O_NOINHERIT` (the close-on-exec analog) and dropping `fchmod` (the creating open already owns the file); the POSIX branch is unchanged. This is the only site in `src/` using those identifiers (`quic_tls_session.cpp` is clean). POSIX-branch syntax check with the interop define passes locally; cpplint clean. - [ ] Full Windows tls-yes CI evidence past the interop provider build (SNI/rotation scenarios and the tls-yes public audits remain first-exposure). + +## Stage 13: Windows QUIC fixture polls (2026-10-30) + +- [x] Evidence (mingw64 job at db7cefec, 41112829610): with the stage-12 keylog fix the tls-yes `make -j2` advanced past `libquic_interop_provider_a` and failed at the next noinst program: `test/integ/quic_interop_endpoint.cpp:3: 'poll.h' file not found`. +- [x] Repair: the interop endpoint's hand-rolled poll loop now goes through the shared `examples/host_poll.hpp` helper (WSAPoll on Windows, the same contract every external-loop consumer uses), preserving the connection-deadline folding and the one-second park bound; `chmod` of the log directory is POSIX-only. `test/integ/http3_udp_fixture.cpp` — the next `poll.h` includer in the same build set — takes a `_WIN32` branch on the same helper while its POSIX stdin-poll path stays byte-identical (the stdin command channel is driven only by the POSIX parity lanes). +- [x] Local gates: both files pass cpplint and `g++ -std=c++20 -fsyntax-only` with the full build flags; `integ/http3_full_gate_runner_test.py` 27/27 (it drives the gate runner that builds and runs these fixtures). +- [ ] Full Windows tls-yes CI evidence past the QUIC fixtures. diff --git a/test/integ/http3_udp_fixture.cpp b/test/integ/http3_udp_fixture.cpp index 39becd588..9d73546f6 100644 --- a/test/integ/http3_udp_fixture.cpp +++ b/test/integ/http3_udp_fixture.cpp @@ -1,5 +1,9 @@ /* Copyright (C) 2026 Sebastiano Merlino; SPDX-License-Identifier: LGPL-2.1-or-later */ +#if defined(_WIN32) +#include "../../examples/host_poll.hpp" +#else #include +#endif #include #include @@ -376,14 +380,22 @@ struct fixture { // its successful completion is committed in the next owner turn. if (executor.pending() || (send && send->state()->applied()) || receive->state()->applied()) return true; auto snapshot = backend.interests(); - if (snapshot.wake) snapshot.sockets.push_back(*snapshot.wake); - std::vector descriptors{{STDIN_FILENO, POLLIN, 0}}; - for (auto& interest : snapshot.sockets) - descriptors.push_back({static_cast(interest.handle.value), static_cast((interest.readable ? POLLIN : 0) | (interest.writable ? POLLOUT : 0)), 0}); auto due = lifetime; for (auto& [id, connection] : connections) if (auto deadline = connection->deadline()) due = std::min(due, *deadline); if (snapshot.next_deadline) due = std::min(due, *snapshot.next_deadline); +#if defined(_WIN32) + // The stdin command channel is driven only by the POSIX parity + // lanes; on Windows the fixture builds but never serves commands. + const auto park = std::min(due, clock_type::now() + std::chrono::milliseconds(1000)); + if (!backend.dispatch(v3_host::wait(snapshot, park), clock_type::now()).ok()) + throw std::runtime_error("backend dispatch failed"); + return true; +#else + if (snapshot.wake) snapshot.sockets.push_back(*snapshot.wake); + std::vector descriptors{{STDIN_FILENO, POLLIN, 0}}; + for (auto& interest : snapshot.sockets) + descriptors.push_back({static_cast(interest.handle.value), static_cast((interest.readable ? POLLIN : 0) | (interest.writable ? POLLOUT : 0)), 0}); int timeout = std::max(std::int64_t{0}, std::chrono::ceil(due - clock_type::now()).count()); if (poll(descriptors.data(), descriptors.size(), timeout) < 0) throw std::runtime_error("poll failed"); if (descriptors[0].revents & (POLLIN | POLLHUP)) { @@ -401,6 +413,7 @@ struct fixture { } if (!backend.dispatch(events, clock_type::now()).ok()) throw std::runtime_error("backend dispatch failed"); return true; +#endif } }; } // namespace diff --git a/test/integ/quic_interop_endpoint.cpp b/test/integ/quic_interop_endpoint.cpp index eff667dc0..7883777ba 100644 --- a/test/integ/quic_interop_endpoint.cpp +++ b/test/integ/quic_interop_endpoint.cpp @@ -1,6 +1,5 @@ /* Copyright (C) 2026 Sebastiano Merlino; SPDX-License-Identifier: LGPL-2.1-or-later */ #include -#include #include #include #include @@ -20,6 +19,7 @@ #include #include #include "../support/http3_network_owner.hpp" +#include "../../examples/host_poll.hpp" #include "../unit/io_udp_backend_contract.hpp" namespace { volatile std::sig_atomic_t stopping = 0; @@ -294,30 +294,16 @@ struct fixture { if (executor.pending() || (send && send->state()->applied()) || receive->state()->applied()) return true; auto snapshot = backend.interests(); - if (snapshot.wake) - snapshot.sockets.push_back(*snapshot.wake); - std::vector descriptors; - for (auto& interest : snapshot.sockets) - descriptors.push_back( - {static_cast(interest.handle.value), static_cast((interest.readable ? POLLIN : 0) | (interest.writable ? POLLOUT : 0)), 0}); auto due = lifetime; for (auto& [id, connection] : connections) if (auto deadline = connection->deadline()) due = std::min(due, *deadline); if (snapshot.next_deadline) due = std::min(due, *snapshot.next_deadline); - int timeout = std::min(1000, std::max(std::int64_t{0}, std::chrono::ceil(due - clock_type::now()).count())); - if (poll(descriptors.data(), descriptors.size(), timeout) < 0 && !stopping) - throw std::runtime_error("poll failed"); - std::vector events; - for (unsigned i = 0; i < snapshot.sockets.size(); ++i) - if (descriptors[i].revents) { - auto& interest = snapshot.sockets[i]; - auto revents = descriptors[i].revents; - events.push_back({interest.key, interest.generation, static_cast(revents & POLLIN), static_cast(revents & POLLOUT), - static_cast(revents & POLLHUP), static_cast(revents & (POLLERR | POLLNVAL))}); - } - if (!backend.dispatch(events, clock_type::now()).ok()) + // Park at most one second past the earliest due work; the shared + // host helper owns the platform poll (WSAPoll on Windows). + const auto park = std::min(due, clock_type::now() + std::chrono::milliseconds(1000)); + if (!backend.dispatch(v3_host::wait(snapshot, park), clock_type::now()).ok()) throw std::runtime_error("backend dispatch failed"); return true; } @@ -333,7 +319,9 @@ int main() { hqnet::files root_check(env("LIBHTTPSERVER_INTEROP_WWW", "/www")); const auto logs = env("LIBHTTPSERVER_INTEROP_LOGS", "/logs"); std::filesystem::create_directories(logs); +#if !defined(_WIN32) if (chmod(logs.c_str(), 0700) != 0) throw std::runtime_error("log permissions failed"); +#endif const auto pcap = logs + "/packets.pcap"; std::signal(SIGTERM, stop); std::signal(SIGINT, stop);