Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/v3-package-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/v3-release-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ jobs:
freebsd:
runs-on: ubuntu-24.04
needs: prepare
timeout-minutes: 180
timeout-minutes: 300
outputs:
artifact_id: ${{ steps.proof.outputs.artifact-id }}
artifact_digest: ${{ steps.proof.outputs.artifact-digest }}
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
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
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
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
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.
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.
Expand Down
9 changes: 9 additions & 0 deletions docs/external-loop-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions examples/host_poll.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ inline std::vector<srv::readiness_event> wait(
const auto ready = ::WSAPoll(fds.data(), static_cast<ULONG>(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
Expand Down
3 changes: 2 additions & 1 deletion scripts/audit-v3-installed-package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
13 changes: 10 additions & 3 deletions scripts/check-v3-public-http2.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,23 @@ 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)
graph = audit.dependency_graph([str(executable)]); check_linkage(graph, linkage, prefix)
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)
Expand Down
10 changes: 7 additions & 3 deletions scripts/check-v3-public-tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
9 changes: 6 additions & 3 deletions scripts/lib/package_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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() + '"']
Expand All @@ -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])
Expand Down
12 changes: 12 additions & 0 deletions scripts/lib/package_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion scripts/lib/release_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
7 changes: 5 additions & 2 deletions scripts/run-v3-http3-full-gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions scripts/test-v3-installed-consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading
Loading