Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
f649968
tests: add an mc/dc white-box driver for src/dtls.c
danielinux Sep 2, 2026
d0093e2
tests: white-box for src/crl.c, and fix the include order in both
danielinux Sep 2, 2026
5f568ac
tests: white-box for src/ocsp.c
danielinux Sep 2, 2026
d9f8aa9
tests: white-box the cipher suite table in src/internal.c
danielinux Sep 2, 2026
404492c
tests: group the orphaned ocsp tests and bound-check DecodeUrl hosts
danielinux Sep 2, 2026
ef2eac6
tests: drive the crl missing-callback guards to a true decision
danielinux Sep 2, 2026
d73917c
tests: drive the crl load and store paths with a real CRL
danielinux Sep 2, 2026
8246542
tests: white-box src/wolfio.c with a mocked byte source and socketpair
danielinux Sep 2, 2026
9673b10
tests: drive CheckOcspResponder's identity chains
danielinux Sep 2, 2026
3d86606
tests: white-box MatchDomainName's wildcard rules
danielinux Sep 2, 2026
c64298b
tests: white-box the handshake ordering police
danielinux Sep 2, 2026
43f8498
tests: white-box the record header's version tolerance
danielinux Sep 2, 2026
1a67037
tests: white-box the revocation, transport and key-schedule leaves
danielinux Sep 3, 2026
f03cb1d
tests: drive DoHandShakeMsgType's pre-dispatch state guards
danielinux Sep 3, 2026
ea3f568
tests: corrupt the handshake in flight over memio
danielinux Sep 3, 2026
65d20cd
tests: corrupt DTLS handshakes in flight, and record what it does not…
danielinux Sep 3, 2026
8bbf3a7
tests: a scheduling datagram transport for DTLS, with 24 packet forge…
danielinux Sep 3, 2026
2396aa7
tests: a generative DTLS ClientHello factory over the packet transport
danielinux Sep 3, 2026
843a01b
tests: argument guards for the public Connection ID API
danielinux Sep 3, 2026
4c15ee0
tests: fence the four CID calls that crash an unpatched library
danielinux Sep 3, 2026
cde6ac3
tests: exercise the public ECH configuration API
danielinux Sep 3, 2026
7e31679
tests: argument guards across the newly-visible ssl_api surface
danielinux Sep 3, 2026
a3ccf97
tests: OCSP stapling accessors and a mocked CRL transport
danielinux Sep 3, 2026
a405ccd
tests: null-argument burn-down across the public API
danielinux Sep 3, 2026
a1a7bd4
tests: null vectors aimed at named operands, not first arguments
danielinux Sep 3, 2026
9853cbc
tests: give each guard the object its accepting half needs
danielinux Sep 3, 2026
a52d92f
tests: file-load failure arms, including a FIFO for the unseekable case
danielinux Sep 3, 2026
54357b0
tests: allocation-failure sweep, and what it found
danielinux Sep 3, 2026
6c77535
tests: white-box more internal.c static null-guards, and fix two that…
danielinux Sep 3, 2026
94ccaaa
tests: pair the lower ValidityTimeFormat bound with a pre-1950 vector
danielinux Sep 3, 2026
50dea6d
tests: guard RPK and DTLS-CID symbols that broke every other module's…
danielinux Sep 3, 2026
bc53038
tests: fix four -Werror classes that broke CI on every job
danielinux Sep 3, 2026
178c064
tests: address Copilot review on the DTLS, OCSP and CRL white-boxes
danielinux Sep 3, 2026
d20bf29
tests: fix two undefined references that broke unit.test linking
danielinux Sep 3, 2026
70464e2
tests: fix three more -Werror classes found in the full CI sweep
danielinux Sep 3, 2026
514709c
tests: enable the CID NULL-argument vectors now that upstream guards …
danielinux Sep 4, 2026
be19488
tests: fix two leaks and the SNI server-only guard
danielinux Sep 4, 2026
c278adc
tests: white-box the DTLS 1.3 role decisions, and CRL/OCSP null guards
danielinux Sep 4, 2026
14d3ff9
tests: public-API argument NULLs, one call per named operand
danielinux Sep 4, 2026
794ebec
tests: session lifecycle guards, clearing the post-rebase ssl_sess.c …
danielinux Sep 4, 2026
011efca
tests: DTLS API and X509 accessor argument guards
danielinux Sep 4, 2026
7b59e42
tests: widen the allocation-failure workload, and record the small-st…
danielinux Sep 4, 2026
1b20726
tests: white-box the certificate error classification in internal.c
danielinux Sep 4, 2026
7e8c333
tests: white-box the certificate-status and send-path error decisions
danielinux Sep 4, 2026
70a3604
tests: fake the revocation back ends to drive the leaf-revocation dec…
danielinux Sep 4, 2026
540e4bf
tests: drive the OCSP cache lookup and mock the responder replies
danielinux Sep 4, 2026
8705774
tests: fail one crypto operation on demand to reach the verify guards
danielinux Sep 4, 2026
1fc0f5c
tests: name the renegotiation_info extension and widen two skip messages
danielinux Sep 4, 2026
02eb080
tests: fix the build guards the narrow CI configurations broke
danielinux Sep 4, 2026
3f6658a
tests: cover the LMS and XMSS bounds checks the rebase brought in
danielinux Sep 6, 2026
1ee3588
tests: build the CA bucket GetCAByName's second operand needs
danielinux Sep 7, 2026
885284e
tests: guard the API-availability macros the no-TLS builds need
danielinux Sep 7, 2026
a952009
tests: restore the CRL mock's counters after the rebase move
danielinux Sep 8, 2026
03206d6
tests: record the certman white-box as smoke-covered
danielinux Sep 8, 2026
717bfb1
tests: compile the CRL mock only where its caller is
danielinux Sep 8, 2026
ef08af0
tests: run the allocation sweep on small-stack builds too
danielinux Sep 9, 2026
08ee741
tests: check API availability guards before the build does
danielinux Sep 9, 2026
ea10d41
tests: pin the session-cache ring-start conditions
danielinux Sep 9, 2026
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
15 changes: 15 additions & 0 deletions .github/workflows/whitebox-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
# check-api-guards.py diffs against the base branch, so it
# needs more than the default shallow checkout.
fetch-depth: 0

- name: Install build dependencies
run: |
Expand All @@ -51,6 +55,17 @@ jobs:
CPPFLAGS=-DWOLFSSL_TEST_STATIC_BUILD
make -j"$(nproc)"

- name: Check API availability guards
# tests/api is one binary built in every CI configuration, so a test
# that calls an API the build did not compile is a link error that
# takes the whole binary down -- and it is invisible to anything that
# only reads headers, because plenty of API is declared unconditionally
# and implemented under a narrower condition. This checks the call
# sites this branch changed against the conditions the implementations
# actually carry. It costs a second and needs no build, so it runs
# before the smoke build rather than after it.
run: python3 tests/api/check-api-guards.py origin/${{ github.base_ref || 'master' }}

- name: Run white-box smoke
# smoke-expected.txt is generated with gcc; six TUs build under clang
# and not gcc, so the compiler has to match or the run reports false
Expand Down
17 changes: 9 additions & 8 deletions tests/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -42104,14 +42104,15 @@ TEST_CASE testCases[] = {
TEST_DECL_GROUP("ocsp", test_ocsp_certid_dup),
TEST_DECL_GROUP("ocsp", test_ocsp_resp_find_status_serial_prefix),
TEST_DECL(test_ocsp_tls_cert_cb),
TEST_DECL(test_ocsp_status_request_v2_multi_revoked_single),
TEST_DECL(test_ocsp_cert_unknown_crl_fallback),
TEST_DECL(test_ocsp_cert_unknown_crl_fallback_nonleaf),
TEST_DECL(test_ocsp_no_url_policy),
TEST_DECL(test_tls13_nonblock_ocsp_low_mfl),
TEST_DECL(test_ocsp_ctx_request_cache),
TEST_DECL(test_ocsp_responder),
TEST_DECL(test_wolfIO_DecodeUrl_crlf_reject),
TEST_DECL_GROUP("ocsp", test_ocsp_status_request_v2_multi_revoked_single),
TEST_DECL_GROUP("ocsp", test_ocsp_cert_unknown_crl_fallback),
TEST_DECL_GROUP("ocsp", test_ocsp_cert_unknown_crl_fallback_nonleaf),
TEST_DECL_GROUP("ocsp", test_ocsp_no_url_policy),
TEST_DECL_GROUP("ocsp", test_tls13_nonblock_ocsp_low_mfl),
TEST_DECL_GROUP("ocsp", test_ocsp_ctx_request_cache),
TEST_DECL_GROUP("ocsp", test_ocsp_responder),
TEST_DECL_GROUP("ocsp", test_wolfIO_DecodeUrl_crlf_reject),
TEST_DECL_GROUP("ocsp", test_wolfIO_DecodeUrl_host_bounds),
TEST_TLS_DECLS,
TEST_TLS_BOUNDS_DECLS,
TEST_TLS_MSGTYPE_DECLS,
Expand Down
212 changes: 212 additions & 0 deletions tests/api/check-api-guards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
#
# check-api-guards.py [--all] [base-ref]
#
# Every entry in tests/api is compiled into the one unit.test binary, in every
# configuration CI builds. A test that calls an API the build did not compile
# is not a test failure -- it is a link error that takes the whole binary down,
# and it is invisible to anything that only reads headers, because wolfSSL
# declares plenty of API unconditionally and implements it under a narrower
# condition. That combination has broken CI here four separate times: a guard
# that names what the test NEEDS rather than what the build PROVIDES.
#
# This checks the other direction. For each API below it walks every call site's
# enclosing #if chain and requires the macros the IMPLEMENTATION requires. Run
# it over the test sources; it exits non-zero if a call site is not covered.
#
# Adding an entry: find where the function is defined (not declared) and copy
# the conditions around it. "requires_off" are macros that must be excluded,
# "requires_on" macros that must be present.
#
# It is deliberately a whitelist rather than a parse of ssl.h: the mapping from
# symbol to implementation guard cannot be derived from the declaration, which
# is the entire problem it exists to catch.
#
# By default it only looks at call sites on lines this branch added or changed
# against the base ref (origin/master), which is what makes it usable as a
# pre-push check. Running it over everything reports plenty of long-standing
# call sites that are fine in practice, because the configurations that would
# break them are not built -- auditing those is a different job. --all does
# that anyway.
import re
import sys
import glob

# symbol -> (must be excluded, must be defined), from the definition site
API = {
# src/ssl.c, under !NO_WOLFSSL_CLIENT && !NO_TLS
'wolfSSLv23_client_method': (['NO_TLS', 'NO_WOLFSSL_CLIENT'], []),
'wolfSSLv23_server_method': (['NO_TLS', 'NO_WOLFSSL_SERVER'], []),
# src/tls.c, additionally under WOLFSSL_DTLS && !WOLFSSL_NO_TLS12:
# DTLS 1.2 is built out of the TLS 1.2 code
'wolfDTLSv1_2_client_method': (['NO_WOLFSSL_CLIENT', 'WOLFSSL_NO_TLS12'],
['WOLFSSL_DTLS']),
'wolfDTLSv1_2_server_method': (['NO_WOLFSSL_SERVER', 'WOLFSSL_NO_TLS12'],
['WOLFSSL_DTLS']),
# src/ssl_api_ext.c: each sits under !NO_TLS as well as its own feature
'wolfSSL_UseSNI': (['NO_TLS'], ['HAVE_SNI']),
'wolfSSL_CTX_UseSNI': (['NO_TLS'], ['HAVE_SNI']),
'wolfSSL_SNI_GetRequest': (['NO_TLS', 'NO_WOLFSSL_SERVER'], ['HAVE_SNI']),
'wolfSSL_SNI_GetFromBuffer': (['NO_TLS', 'NO_WOLFSSL_SERVER'], ['HAVE_SNI']),
'wolfSSL_UseSupportedCurve': (['NO_TLS'], ['HAVE_SUPPORTED_CURVES']),
'wolfSSL_CTX_UseSupportedCurve': (['NO_TLS'], ['HAVE_SUPPORTED_CURVES']),
# wolfcrypt/src/memory.c, under USE_WOLFSSL_MEMORY -- which --enable-leantls
# removes by way of WOLFSSL_LEANPSK
'wolfSSL_SetAllocators': ([], ['USE_WOLFSSL_MEMORY']),
'wolfSSL_GetAllocators': ([], ['USE_WOLFSSL_MEMORY']),
}


def strip_comments(text):
"""Blank out comments and string literals, keeping every newline so line
numbers still line up. Without this the scan matches the API names in the
explanatory comments these tests are full of."""
out = []
i, n = 0, len(text)
while i < n:
c = text[i]
if c == '/' and i + 1 < n and text[i + 1] == '*':
j = text.find('*/', i + 2)
j = n if j < 0 else j + 2
out.append(''.join(ch if ch == '\n' else ' ' for ch in text[i:j]))
i = j
elif c == '/' and i + 1 < n and text[i + 1] == '/':
j = text.find('\n', i)
j = n if j < 0 else j
out.append(' ' * (j - i))
i = j
elif c in '"\'':
q, j = c, i + 1
while j < n and text[j] != q:
j += 2 if text[j] == '\\' else 1
j = min(j + 1, n)
out.append(''.join(ch if ch == '\n' else ' ' for ch in text[i:j]))
i = j
else:
out.append(c)
i += 1
return ''.join(out)


def guard_chain(lines, upto):
"""The #if directives open at line `upto`, joined, continuations included."""
stack = []
for i, line in enumerate(lines[:upto], 1):
s = line.strip()
if re.match(r'#\s*if', s):
text, j = [], i - 1
while True:
text.append(lines[j].strip())
if not lines[j].rstrip().endswith('\\'):
break
j += 1
stack.append(' '.join(text))
elif re.match(r'#\s*endif', s):
if stack:
stack.pop()
elif re.match(r'#\s*el(se|if)', s):
if stack:
stack[-1] = s
return ' '.join(stack)


# Macros that are only ever defined in a build that has TLS, so a block already
# guarded by one of them cannot also need !defined(NO_TLS) spelled out.
IMPLIES_TLS = (
'WOLFSSL_TLS13', 'WOLFSSL_DTLS', 'WOLFSSL_DTLS13', 'HAVE_SNI', 'HAVE_ALPN',
'HAVE_SESSION_TICKET', 'HAVE_SECURE_RENEGOTIATION', 'HAVE_MAX_FRAGMENT',
'HAVE_SUPPORTED_CURVES', 'HAVE_EXTENDED_MASTER', 'HAVE_TRUSTED_CA',
'HAVE_ENCRYPT_THEN_MAC', 'HAVE_SERVER_RENEGOTIATION_INFO',
'HAVE_CERTIFICATE_STATUS_REQUEST', 'HAVE_TLS_EXTENSIONS', 'HAVE_ECH',
)


def require_ref(base):
"""Refuse to run against a ref git cannot resolve.

A shallow checkout has no base branch, and then every diff comes back
empty and the check passes without looking at anything -- which is worse
than not running it, because it reports success. Fail loudly instead.
"""
import subprocess
r = subprocess.run(['git', 'rev-parse', '--verify', '--quiet', base + '^{commit}'],
capture_output=True, text=True)
if r.returncode != 0:
sys.stderr.write(
f"check-api-guards: cannot resolve '{base}'.\n"
f" The diff scope needs it. In CI, check out with fetch-depth: 0;\n"
f" locally, fetch the base branch, or pass --all to scan every\n"
f" call site instead.\n")
sys.exit(2)


def changed_lines(path, base):
"""Line numbers this branch added or changed in path."""
import subprocess
out = subprocess.run(['git', 'diff', '-U0', f'{base}...HEAD', '--', path],
capture_output=True, text=True).stdout
hit = set()
for m in re.finditer(r'^@@ -\S+ \+(\d+)(?:,(\d+))? @@', out, re.M):
start = int(m.group(1))
count = int(m.group(2)) if m.group(2) else 1
hit.update(range(start, start + count))
return hit


def check(path, only=None):
lines = strip_comments(open(path, errors='replace').read()).split('\n')
bad = []
for i, line in enumerate(lines, 1):
if only is not None and i not in only:
continue
for sym, (off, on) in API.items():
if not re.search(r'\b' + re.escape(sym) + r'\s*\(', line):
continue
chain = guard_chain(lines, i)
miss_off = [m for m in off
if f'!defined({m})' not in chain and f'ifndef {m}' not in chain]
miss_on = [m for m in on
if f'defined({m})' not in chain and f'ifdef {m}' not in chain]
# WOLFSSL_DTLS implies TLS is compiled in, so a DTLS-guarded block
# never needs !NO_TLS spelled out as well.
if any(f'defined({m})' in chain or f'ifdef {m}' in chain
for m in IMPLIES_TLS):
miss_off = [m for m in miss_off if m != 'NO_TLS']
if miss_off or miss_on:
bad.append((i, sym, miss_off, miss_on))
return bad


def main():
args = [a for a in sys.argv[1:]]
scan_all = '--all' in args
if scan_all:
args.remove('--all')
base = args[0] if args else 'origin/master'
if not scan_all:
require_ref(base)
paths = sorted(glob.glob('tests/api/test_*.c'))
total = 0
for path in paths:
only = None if scan_all else changed_lines(path, base)
if only is not None and not only:
continue
for line, sym, off, on in check(path, only):
need = []
if off:
need.append('!defined(' + '), !defined('.join(off) + ')')
if on:
need.append('defined(' + '), defined('.join(on) + ')')
print(f'{path}:{line}: {sym} needs {" and ".join(need)}')
total += 1
scope = 'every call site' if scan_all else f'call sites changed since {base}'
if total:
print(f'\n{total} call site(s) reachable in a build that does not '
f'implement the API ({scope})')
return 1
print(f'api guards: {scope} covered')
return 0


if __name__ == '__main__':
sys.exit(main())
3 changes: 2 additions & 1 deletion tests/api/include.am
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ tests_unit_test_SOURCES += tests/api/test_tls13_bounds.c
tests_unit_test_SOURCES += tests/api/test_tls13_features.c
endif

EXTRA_DIST += tests/api/api.h
EXTRA_DIST += tests/api/check-api-guards.py \
tests/api/api.h
EXTRA_DIST += tests/api/api_decl.h
EXTRA_DIST += tests/api/test_md2.h
EXTRA_DIST += tests/api/test_md4.h
Expand Down
Loading
Loading