Skip to content

Mcdc test coverage part 8 - #11355

Open
danielinux wants to merge 55 commits into
wolfSSL:masterfrom
danielinux:mcdc-test-coverage
Open

Mcdc test coverage part 8#11355
danielinux wants to merge 55 commits into
wolfSSL:masterfrom
danielinux:mcdc-test-coverage

Conversation

@danielinux

Copy link
Copy Markdown
Member

No description provided.

Copilot AI lite review requested due to automatic review settings September 2, 2026 14:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new DTLS white-box has configuration-guard/compile-availability issues and the OCSP white-box currently leaks heap entries created by GetOcspEntry().

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds new MC/DC white-box drivers under tests/unit-mcdc/ to exercise previously unreachable file-static logic in src/ocsp.c, src/dtls.c, and src/crl.c, and wires them into the unit-mcdc smoke expectations and distribution list.

Changes:

  • Introduces three new white-box translation units: OCSP, DTLS, and CRL.
  • Updates tests/unit-mcdc/smoke-expected.txt to include the new binaries in the smoke list.
  • Updates tests/include.am to ship the new sources in EXTRA_DIST.
File summaries
File Description
tests/unit-mcdc/test_ocsp_whitebox.c New OCSP white-box driver for MC/DC coverage of internal cache matching logic.
tests/unit-mcdc/test_dtls_whitebox.c New DTLS white-box driver targeting stateless DTLS helpers and argument guards.
tests/unit-mcdc/test_crl_whitebox.c New CRL white-box driver targeting CRL lookup argument-guard conditions.
tests/unit-mcdc/smoke-expected.txt Adds the new white-box binaries to the expected passing set for the smoke script.
tests/include.am Adds the new white-box sources to EXTRA_DIST.
Review details

Suppressed comments (1)

tests/unit-mcdc/test_dtls_whitebox.c:343

  • The skip stub message only mentions WOLFSSL_DTLS, but the driver also depends on DTLS 1.3 and CID helpers (and is excluded under WOLFCRYPT_ONLY). If this TU is skipped due to those feature macros, the current message is misleading for troubleshooting.
int main(void)
{
    printf("dtls white-box: skipped (WOLFSSL_DTLS not built)\n");
    return 0;
  • Files reviewed: 5/5 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/unit-mcdc/test_dtls_whitebox.c
Comment thread tests/unit-mcdc/test_ocsp_whitebox.c
Comment thread tests/unit-mcdc/test_crl_whitebox.c Outdated
Comment thread tests/unit-mcdc/test_ocsp_whitebox.c Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are confirmed portability/build issues in test_wolfio_whitebox.c (unconditional POSIX socket headers and socketpair() usage) and a constant conditional expression in test_internal_clienthello_whitebox.c that may break -Werror builds.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 29/29 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread tests/unit-mcdc/test_wolfio_whitebox.c
Comment thread tests/unit-mcdc/test_wolfio_whitebox.c
Comment thread tests/unit-mcdc/test_internal_clienthello_whitebox.c Outdated
Every uncovered condition in dtls.c is in a file-static helper on the stateless
path, where the public API fixes most arguments: no caller can ask
CreateDtls12Cookie for a NULL secret or FindExtByType for a length that
overruns its own vector. The dtls group already runs 87 of 103 and still leaves
46 of 56 uncovered, so the API limit is reached before the file is.

Drives CreateDtls12Cookie, FindExtByType, ClientHelloSanityCheck,
TlsCheckSupportedVersion and DtlsCidGetSize with paired vectors in one binary.
dtls.c 10/56 -> 16/56.
crl.c measured 3 of 48: there is no crl test group, CRL lives in certman, and
certman runs 11 of 36 on this option list. Drives CheckCertCRLCm's
(serial == NULL || serialSz == 0) && serialHash == NULL guard with the four
vectors MC/DC needs. crl.c 3/48 -> 6/48.

Both drivers were including the target .c before wolfssl/options.h. config.h
carries no feature macros, so the smoke build compiled them with WOLFSSL_DTLS
and HAVE_CRL undefined: they took their skip stubs, exited 0, and were recorded
as passing while testing nothing. options.h now comes first, which is the rule
AGENTS.md already states. Under the smoke build they now drive 22 and 4 vectors
respectively.
The ocsp group runs 3 of its 8 tests on the campaign option list, so most of
ocsp.c is never entered from tests/api. GetOcspEntry's cache-match compares an
entry against the request by issuer hash and issuer key hash; a caller coming
through the public API builds both from the same certificate, so neither
operand has a false case from outside and the loop body needs a seeded cache
to execute at all. ocsp.c 7/47 -> 9/47.
InitSuites is a long table of 'tls1_2 && haveX && haveAES128' rows over twelve
have* flags. No handshake can drive it: every caller derives those flags from
what the build compiled in, so on one binary they are constant and no operand
has an independence pair. A one-at-a-time sweep from an all-ones baseline, over
six protocol versions and both sides, gives the pair for every operand of every
row in n+1 calls instead of 2^n. 156 calls, internal.c 532/1730 -> 573/1722.

Also extends the crl driver to CheckCertCRLCm's cm != NULL && cm->cbMissingCRL
and cm != NULL && cm->crlCb guards, whose operand 0 is true by construction
from wolfSSL_CertManagerCheckCRL.
Eight ocsp tests were registered with a bare TEST_DECL and no group, and
ApiTest_RunGroup selects on group != NULL, so none of them has ever run under
--group. Among them are both OCSP-to-CRL fallback tests and the DecodeUrl CR/LF
injection test. Grouping them takes the ocsp group from 3 of 8 running to 9 of
17, and src/wolfio.c from 18/87 to 43/87.

Adds test_wolfIO_DecodeUrl_host_bounds for the two host-parsing loops, whose
four operands are each ended by a different vector: a bracketed IPv6 literal
with no closing bracket, one cut short by a NUL, a host running to the end of
the buffer, and a host longer than the item cap, with well-formed URLs as the
accepting partners. Those are attacker-controlled shapes that no in-tree caller
produces.
The previous vectors reached CheckCertCRLCm's cm != NULL && cm->cbMissingCRL
and cm != NULL && cm->crlCb chains and covered nothing, because every vector
took the decision false: once by short-circuit on a NULL cm, once because no
callback was installed. An operand that changes value without changing the
decision outcome has no independence pair.

Installs both callbacks so the decisions go true, and drives the error
callback's own return value both ways for the third operand of the :686 chain,
plus a url longer than the 256-byte stack buffer for the copy guard inside the
:668 body. crl.c 6/48 -> 11/48.
BufferLoadCRL and BufferStoreCRL are argument-validated with wide OR chains and
then branch on DER vs PEM. Every in-tree caller passes a real CRL and a real
type, so the rejecting side of each operand is unreachable from outside, while
the accepting side needs a genuinely parsed entry: a hand-built one has no
toBeSigned or signature, so it can only take the :1036 guard true and leaves
the whole store path uncovered.

Loads certs/crl/crl.der through the real loader and lets the CRL context own
the entry, then stores it as DER, as PEM, into an undersized buffer and with a
size query. crl.c 11/48 -> 18/48.
wolfio.c reads as needing a transport and does not. It needs a byte source and
a descriptor, and both can be supplied locally.

wolfIO_HttpProcessResponseGenericIO takes a WolfSSLGenericIORecvCb, which is
int (*)(char*, int, void*), so the whole HTTP response state machine is
drivable from a memory buffer. The mock caps bytes per call, which forces the
reassembly loop to iterate and produces split headers and split chunks that a
real socket would not reproduce on demand, and it can return -1 to inject a
read error with no transport involved.

socketpair(AF_UNIX) covers the descriptor half: wolfIO_SockIsDGram wants an fd,
not a peer, and a stream pair, a datagram pair and a closed descriptor give its
getsockopt branch all three outcomes. No ports, no DNS, no listener.

wolfio.c 43/87 -> 47/87.
CheckOcspResponder takes an OcspResponse and four raw hashes, walks bs->single
and compares bytes. Nothing is stored, so the response can be a local -- unlike
GetOcspEntry, which links what it is given into ocsp->ocspList, a list the
library allocates and frees, where a stack fixture crashes.

Each vector breaks a chain at a different operand: no key hash offered, name
mismatch, key mismatch, and the delegated-responder arm behind the OCSP-signing
usage bit, with a fully matching vector as the accepting partner. A real
response is self-consistent, so the mismatch cases have no independence pair
from outside. ocsp.c 11/47 -> 18/47.
MatchDomainName decides whether a presented certificate name matches the host
being connected to. It is pure -- two strings, two lengths, a flags word, no
ssl, no allocation -- but nearly uncovered, because callers reach it only after
a completed verification: the name comes from a parsed SAN or CN and the host
from local configuration, so an empty pattern, a bare star, a wildcard that is
not leftmost, a wildcard with no dot after it, or a zero length on one side
only never arrive. Those are the inputs an attacker picks.

38 vectors over both wildcard-policy flag settings, each row flipping one named
operand with an accepting partner differing in a single field.
internal.c 573/1722 -> 587/1722.
SanityCheckMsgReceived refuses a handshake message that arrives on the wrong
side, arrives twice, or arrives out of order. Every condition in it is a
rejection, so a conforming peer takes all of them false and no number of
handshakes produces an independence pair; the vectors have to be malformed on
purpose.

The fixture is a zeroed WOLFSSL with its ctx pointed at a client CTX -- the
function reads only options, msgsReceived and specs, and dereferences nothing
else unguarded except SSL_CM(ssl)->ocspMustStaple in the ServerHelloDone arm.
No certificate, no transport, no peer.

Sweeping from all-clear and all-set alone measured 15 conditions: both
saturated ends are refused by the prerequisite or the duplicate check before
the out-of-order chain is ever evaluated. Each arm therefore also gets the
state in which its message is accepted, and one flipped bit from there puts
exactly one chain operand true with the rest false. That took it to 47.

3552 calls. internal.c 587/1722 -> 630/1722.
GetRecordHeader is the first code to look at bytes off the wire, before any
key or MAC. Its decisions are the version-mismatch tolerance rules -- which
peer, in which handshake state, may send which version -- plus the length and
record-type checks.

From tests/api the header always comes from wolfSSL's own record writer, so
pvMajor and pvMinor equal ssl->version on every call and the whole mismatch
block is dead: downgrade, connectState, acceptState and the
alert-before-negotiation carve-out are evaluated only for a header the local
writer never produces.

The fixture is five bytes and a struct. inputBuffer.buffer is a pointer, so it
points at a local array; the function writes only through its out-parameters
and stores nothing. Rows are one flip from a well-formed TLS 1.2 record, with
the baseline re-run between rows so the DTLS replay window does not carry.

61 calls. internal.c 630/1722 -> 642/1722.
Four batches, measured together.

crl.c 18->29. CompareCRLnumber over hex strings a parser would never emit
(non-hex, empty, a number that went backwards); FindRevokedSerial with a
same-length different-serial vector; BufferStoreCRL's five-operand guard,
driven by hand-linking a CRL_Entry that is missing each field in turn and
unlinking it before FreeCRL walks the list; LoadCRL over a real directory and
over NULL arguments.

wolfio.c 47->53. wolfIO_DecodeUrl against the malformed URLs an attacker
supplies and a certificate never carries: unterminated IPv6 bracket, CR/LF
smuggled into the host, a host and a port at the length cap, a colon with no
digits, a port past the 16-bit ceiling. MAX_URL_ITEM_SIZE is private to
wolfio.c, so the API test has to mirror it and hope; the white-box does not.

ocsp.c 18->21. A CbOCSPIO mock is a complete responder for this function --
it can return a negative error, a zero-length body, or a positive length with
a NULL buffer on demand, which no real responder can be made to do.

keys.c 26->33. The file has no group and no caller outside internal.c and
tls13.c, and everything in it runs from values a handshake has already fixed:
one cipher suite, one version, enc and dec never NULL. GetCipherSpec swept
over every suite-table selector, SetKeys called twice on the same objects so
the lazy-allocation guards get both halves, SetKeysSide over the four
dtls/1.3 combinations.
The guards run before any parser, so the message body can be zeros; they all
call SendAlert on the way out, which is the only reason this needs more than
the zeroed fixture -- a send callback that consumes and discards, or the alert
path dereferences a NULL CBIOSend.

internal.c 642/1722 -> 644/1722. Small: most of this function's 22 uncovered
conditions sit after the dispatch, inside the per-message parsers, not in the
guards.
The error paths in internal.c are most of what is left uncovered there, and a
conforming pair of endpoints never enters them. Reaching them needs a peer
that sends something wrong -- not a transport.

test_memio already runs both endpoints through a byte buffer with credentials
from certs/, and both sides are ours, so the buffer between them can be edited
between rounds. test_tls_wire_mangle flips a bit at a chosen offset of a
chosen round, which selects which handshake message gets hit and where: record
type, record length, handshake type, handshake length, or inside the body.
test_tls_wire_sequence uses the harness's own drop, duplicate, reorder and
length-rewrite helpers, which reach the ordering and retransmit logic a byte
flip cannot -- a flipped byte still arrives once, in sequence, at the right
length.

Both assert only that a corrupted handshake fails rather than crashes; the
coverage is in the paths it takes on the way out. Both run in the ssl_hs
group, which five of the six protocol modules already measure.

internal.c 650/1722 -> 690/1722. The DTLS arms of both tests currently
contribute nothing -- see the commit that follows.
… reach

Same mechanism as the TLS wire mangler, with the offsets named after DTLS
framing rather than TLS: a DTLS record header is thirteen bytes, and the
handshake header carries a message sequence, fragment offset and fragment
length that TLS has no equivalent of. Replay, drop and reorder are included
because DTLS is built to tolerate them, so they enter the retransmit pool and
the reassembler rather than being refused.

Two mistakes are fixed here and worth keeping written down. The endpoints are
stepped by hand rather than through test_memio_do_handshake, which runs the
client and the server in one round and leaves nothing in flight to corrupt --
the first version passed in 2.3 seconds and measured nothing. And the offsets
now reach past the fixed header into the extension block.

It still earns zero. Three measurements put dtls.c at exactly 16/56 and
dtls13.c at exactly 70/132 every time; an identical number means the code is
not entered, not that the vectors are weak. dtls.c's residue is entirely
inside SendStatelessReplyDtls13's extension parsing, and a corrupted DTLS
record is dropped by the record layer before that parser sees it -- which is
the tolerance DTLS exists to provide. Reaching it needs a well-formed record
carrying a malformed extension block: a built ClientHello, not a corrupted
one. The sweep is kept narrow so it costs seconds until that fixture exists.
…ries

test_memio is a byte stream; DTLS is datagrams, and every guardrail in the
protocol is about which datagram arrives, in what order, how many times, and
carrying which epoch and sequence number. This replaces the transport rather
than editing its buffer: each datagram the stack sends is captured whole, its
record header parsed, and a policy consulted before it is queued -- deliver,
drop, hold for n rounds, duplicate, rewrite a header field, coalesce with its
neighbour, truncate. The receiver then reads whole datagrams out of the queue
as a UDP socket would.

24 policies, each named for the guardrail it provokes: replay, sequence number
past and future the replay window, unknown and zeroed epoch, lost flight,
reordering, fragment offset past the message, fragment longer than the
message, overlapping fragments, message sequence forward and backward, record
length longer and shorter than the datagram, content-type confusion, two
records in one datagram, truncation, and body corruption at six depths inside
the extension block plus the echoed cookie.

No key material is needed: the DTLS record header is not encrypted, and every
guardrail above keys off it. A secret callback is wired behind
HAVE_SECRET_CALLBACK for cases that later need a protected body; that macro is
not in the campaign option list, so it compiles out there.

df_run reports whether the handshake actually completed rather than whether
the loop ran -- the first version returned success unconditionally, which is
how a transport that never connects still passes.

This adds no MC/DC on dtls.c, and the export says why: the file is 85% line
covered and 20% MC/DC covered in this variant. SendStatelessReplyDtls13 is
entered -- 247 executed segments in its range -- so the problem is not reach.
The residue needs ClientHellos that are well-formed but semantically
specific: one with no supported_versions, one offering PSK modes, one whose
key share names a group the server does not have, one echoing a corrupted
cookie. Corruption cannot produce those; they have to be built. The builder
exists in tests/unit-mcdc/test_internal_clienthello_whitebox.c and wants
pointing down this transport.
The named hellos were known-answer cases: each says one specific wrong thing.
Testing the parser's limits is a different job, so the factory generates them
-- one policy, one mutation id, driven from a loop -- and each mutation breaks
exactly one invariant while keeping the datagram a datagram, so the parser
reaches the check that invariant belongs to instead of bailing at the door.

23 mutations: extension block one byte long, one byte short, and declared
zero; one, eight and forty unknown extensions; a 900-byte extension; a known
extension with an empty body; supported_versions, key_share and cookie each
duplicated; session id declared 0, 33 and 255; cookie declared 0 and 255;
cipher suite list odd-length, zero and huge; compression list zero and huge;
legacy_version at both extremes. Plus surgery that removes supported_versions,
key_share, psk_key_exchange_modes or pre_shared_key outright, rewrites the
named group to one the server has not got, forces PSK_KE-only and
PSK_DHE_KE-only, and corrupts the echoed cookie -- each fixing up the
extension, handshake, fragment and record lengths so the result is refused on
its meaning rather than its framing.

Also here because neither is expressible as a payload: a small-MTU pass, so
the stack fragments its own ClientHello and the isFirstCHFrag operands are
reachable at all; and a Connection ID pass that calls wolfSSL_dtls_cid_use on
both endpoints, because CID must be negotiated and nineteen of the conditions
left in dtls.c are in CID functions that are never entered otherwise.

dtls.c 16/56 -> 18/56. The surgery is what moved it; six earlier measurements
with corruption alone returned exactly 16/56 every time.
Nineteen of the conditions left in dtls.c were in the CID functions, and a
great deal of packet machinery was pointed at them first and moved none. They
are not protocol behaviour: they are NULL-and-zero argument guards.

    if (ssl == NULL || buf == NULL)                             DtlsCidGet
    if (id == NULL || id->length == 0)
    if (ssl == NULL || cid == NULL)                             DtlsCidGet0
    if (info == NULL || info->rx == NULL || !info->rx->length)  DtlsCIDCheck
    if (ssl == NULL || cid == NULL || size == 0)                DtlsCidReplaceTx
    if (msg == NULL || cidSz == 0 || msgSz < OPAQUE8_LEN + cidSz)

No handshake passes NULL and no forged datagram can make it; the only way to
pair these operands is to call the functions directly. Three ssl states are
needed because "no CID info", "info but no id" and "an id of length zero" are
distinct operands: no ssl, an ssl with CID compiled but never enabled, and an
ssl with CID enabled but not yet negotiated.

The assertions are weak on purpose -- these vectors establish that the guard
is taken and the process survives, not what each function returns for an
argument it is documented to reject.

dtls.c 18/56 -> 26/56.
The campaign reports defects, it does not carry fixes, so src/dtls.c is back
to origin/master and the four NULL-argument calls that segfault cannot run in
the suite -- a crash discards the coverage of every test in the variant.

They are left in the source behind WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED with
the fault of each written next to it, so the gap is visible and re-enabling
them is one define once the library guards them.

dtls.c stays at 26/56: the coverage came from the guards that are reachable,
not from the four that crash.
src/ssl_ech.c measured 0 of 52 conditions -- zero, not "poorly covered" --
even though ECH is compiled in and five ECH tests run in the tls13 group of
the same binary. Those tests drive ECH through a handshake using configs the
harness makes for them; none calls the public configuration API, and that is
where every condition in the file lives: generating a config for a named
KEM/KDF/AEAD, importing one from raw bytes or base64, reading one back into a
caller's buffer, and the size and argument checks on all of it.

The file was invisible to the campaign until this part because ssl_ech.c is
#included into ssl.c rather than compiled standalone, so it produced no object
file and never appeared in a filtered llvm-cov export.

Vectors a handshake cannot produce: a NULL ctx, a buffer one byte too small, a
length of zero, base64 that is not base64, base64 that decodes to something
that is not a config, a KEM triple no build implements, and a retry-config
query on a connection that never negotiated ECH.

ssl_ech.c 0/52 -> 19/52.
ssl_misc.c is 11 conditions of file-helper guards -- a seek that fails, a
length that is zero or absurd, a read shorter than promised -- all in static
helpers, but every public load-from-file entry point runs through them, so the
vector is the FILE rather than the argument.

An empty file gives sz <= 0; a directory and a nonexistent name give the open
and read failures; a file that is not a certificate exercises the parse
failure after a successful read.

The unseekable case needs a FIFO. fopen() on it succeeds, so the XBADFILE arm
is passed, and then fseek() fails with ESPIPE because a pipe has no position
-- the exact shape the guard is written for, and nothing on disk imitates it.
The FIFO is opened read-write by the test first so the library's fopen()
cannot block; a hanging test costs the variant as surely as a crash.

ssl_misc.c 0/11 -> 4/11, ssl_load.c 34/155 -> 35/155.

The short-read arm (XFREAD returning fewer bytes than the seek promised) is
still open and is not reachable this way: once the seek fails the function
returns before reading, and a regular file always reads what it promised.
That one needs fault injection below the C library, not a different file type.
Error propagation is the largest remaining category -- 633 of 2855 uncovered
conditions -- almost all of it 'if (ret != 0)' after a call that cannot fail in
a working configuration. wolfSSL_SetAllocators is the lever: one harness
reaching every out-of-memory arm, since all allocation routes through it. The
sweep fails the Nth allocation and lets the rest succeed, one N per run, so
each vector takes a different arm; failing exactly one rather than everything
from N onward keeps them isolated.

RESULT, and a negative one is still a result: on the default build wolfSSL
survives ALL 37 of this workload's allocation failures. Every one returns an
error and cleans up -- no crash, no wedged state. For a safety case that is
worth stating: those out-of-memory arms are not merely present, they work.

The small-stack variant is excluded because it segfaults during the sweep
while the default completes it. Whether that is a small-stack allocation path
that mishandles failure or the harness tripping something that build is more
sensitive to is NOT established, and a crash there discards the whole
variant's evidence -- so it is fenced until the difference is understood, not
left to take the measurement down.

The harness was inert before this commit for a bug in its own counter:

    if (fi_failAt >= 0 && fi_count++ == fi_failAt)

short-circuits the increment away whenever injection is off, so the counting
pass counted nothing and the sweep bound came out zero. The index is now taken
before the test. That is the same operand short-circuit these tests exist to
cover, in the test's own code.
… never paired

Two more file-static functions (SupportedHashSigAlgo, GetCtxOcspLock,
BuildCertificateStatusWithStatusCB, InvalidateSessionOnFatalAlert,
dtlsRecordIsNewest, FreeCachedHandshakeMessages, ParseCipherList,
SendHandshakeMsg, DecodePrivateKey_ex, GetRealSessionID, EcMakeKey,
SetCurveId, RpkIsTrusted, CopyDecodedPubKey, CopyDecodedSig,
ProcessPeerCertParse, ProcessCSR_ex) get their null-guard conditions closed
via two new drivers (test_internal_nullguard_whitebox.c,
test_internal_peerkey_whitebox.c), plus a third and fourth driver for
HashSkeData/GetDhPublicKey (test_internal_dhskehash_whitebox.c) and
EdDSA_Update (test_internal_eddsa_whitebox.c).

Fixed a real bug in the first two drivers: SendHandshakeMsg's and
DecodePrivateKey_ex's rejecting vectors had no self-contained accepting
partner, on the assumption the campaign union could borrow one from the real
handshake corpus. It cannot -- llvm-cov computes the covered bit per
condition from one binary's own profile, and the campaign's union is a
logical OR of those already-computed bits, not a merge of raw execution
traces, so an accepting call recorded by one binary can never pair with a
rejecting call recorded by another. Confirmed against the real campaign run:
those two decisions, plus ProcessPeerCertParse's three-operand guard, stayed
in GAPS.md after the first union despite being individually exercised by an
isolated local measurement. Every driver here now supplies its own false/true
partner in the same binary; verified locally per-driver and against the real
tls_core aggregate before committing.

src/internal.c union MC/DC: 690/1722 -> 752/1722.
test_wc_MakeCert_generalizedTimeValidity() closed the tm_year < 2050
operand by pushing notAfter past 2050. Add a second wc_MakeCert() call
with a large negative daysValid to push notAfter below 1950 as well,
pairing the tm_year >= 1950 operand (short-circuited false) against the
existing >=1950,<2050 true row from ordinary certificate encodes in the
same test group. Closes both remaining MC/DC operands of that decision;
asn.c union coverage 1488/1580 -> 1490/1580.
… build

tests/api.c is one shared translation unit compiled into every module's
unit.test, filtered by --group only at runtime. Two calls added earlier this
part referenced symbols that do not exist in every module's config, and broke
the shared build for anything without that specific feature on -- not just
the module the test was written for.

test_ssl_cert.c: WOLFSSL_CERT_TYPE_X509 and the *_cert_type / *_expected_rpk
family are declared only under #ifdef HAVE_RPK in wolfssl/ssl.h. The guard on
test_wolfSSL_cert_api_arg_guards was only !NO_CERTS && !NO_WOLFSSL_CLIENT, so
any config without --enable-rpk (asn, pkcs7, and the rest of wolfCrypt-only
modules) failed to compile. Confirmed by inspecting the header's own #ifdef
nesting, then by a real build.

test_ssl_ext.c and test_ssl_cert.c: wolfSSL_dtls_set_pending_peer() is
declared unconditionally in ssl.h but implemented only under
WOLFSSL_DTLS_CID && !WOLFSSL_NO_SOCK (src/ssl_api_dtls.c). The surrounding
#ifdef WOLFSSL_DTLS guard is not sufficient -- a config with WOLFSSL_DTLS on
and WOLFSSL_DTLS_CID off compiles the call and fails at LINK time, not
compile time, which is why tracing the header's #ifdefs alone did not catch
it and a real build was needed. Reproduced against asn's own
ignore_name_constraints and runtime_date_check variants: 'ld.lld: error:
undefined symbol: wolfSSL_dtls_set_pending_peer'.

Verified with real (non-cached) rebuilds of asn, pkcs7 and tls_core after the
fix: all three link and gate clean. asn.c 1490/1580, pkcs7.c 943/1058,
internal.c 752/1722 -- all unchanged from before this fix, confirming it
repairs the build without touching what those modules measure.
CI failed every job on 0940946 with four distinct errors, all in tests
added earlier this part, all from guards that did not match what
wolfssl/ssl.h actually declares:

unused-variable: proto/protoSz were used only under HAVE_ALPN and sz only
under WOLFSSL_DTLS, but all three were declared unconditionally. Wherever
those features are off they are unused, and the tree builds with
-Werror=unused-variable. Declarations now sit under the same guard as their
uses.

implicit-function-declaration / nested-externs / int-conversion:
test_wolfSSL_x509_accessor_guards was guarded on WOLFSSL_CERT_GEN, which is
not what gates that API. ssl.h declares wolfSSL_X509_load_certificate_file,
_get_signature, _get_pubkey_buffer and _free under OPENSSL_EXTRA ||
OPENSSL_EXTRA_X509_SMALL || KEEP_PEER_CERT || KEEP_OUR_CERT ||
SESSION_CERTS. With the wrong macro they were implicit declarations, and the
implied int return then tripped int-conversion on the WOLFSSL_X509*
assignment. Guard now mirrors the header.

implicit-function-declaration: wolfSSL_dtls_set_mtu is declared under
(WOLFSSL_SCTP || WOLFSSL_DTLS_MTU) && WOLFSSL_DTLS. Guarding the call on
WOLFSSL_DTLS_MTU alone left SCTP-only configs calling an undeclared
function.

discarded-qualifiers: wolfSSL_UseALPN takes char*, not const char*, so the
string literals passed to it were a const violation. Uses a mutable buffer
now.

Verified locally before pushing, with -Werror, in the two configs that
reproduce these: one with alpn/sni/dtls/opensslextra all off, and one with
dtls+sctp but no dtls-mtu. Both compile clean.
Four review comments, all correct.

test_dtls_whitebox.c gated only on WOLFSSL_DTLS while calling helpers that
src/dtls.c compiles under narrower guards: TlsCheckSupportedVersion and
SendStatelessReplyDtls13 need WOLFSSL_DTLS13 && !NO_WOLFSSL_SERVER, and
DtlsCidGetSize needs WOLFSSL_DTLS_CID. A DTLS build without 1.3 or without
CID would not compile this driver. Same guard-mismatch class as the CI
failures fixed in the previous commit; it did not show up there only because
tests/unit-mcdc drivers are EXTRA_DIST and not built by the normal make.

test_ocsp_whitebox.c leaked. GetOcspEntry() prepends a heap-allocated entry
and makes it the new head when it finds no match, which vectors 1 and 2 both
do; the driver then re-pointed ocspList at its own stack node, dropping that
pointer. It now frees every heap node ahead of the stack node after each
no-match vector, deriving the heap the same way FreeOcspEntry's caller in
src/ocsp.c does (ocsp->cm->heap -- ocsp itself has no heap member, which the
first attempt at this fix got wrong and the smoke run caught).

Both skip-stub messages named only the feature macro while the guard also
required certs and !WOLFCRYPT_ONLY, so a skip for either of those reasons
reported a misleading cause. That matters more than it looks in this campaign:
a driver that silently skips still exits 0 and reports nothing, and misreading
why has cost real time here.

Smoke suite: 77 passed, 0 failed.
Second CI round narrowed from every job to two clusters, both failing to link
tests/unit.test on the same two symbols.

test_wolfIO_DecodeUrl_host_bounds was defined only inside
#if defined(HAVE_HTTP_CLIENT) but registered in the api.c test table
unconditionally, so wherever HTTP client support is off the table referenced a
symbol with no definition. Its sibling test_wolfIO_DecodeUrl_crlf_reject in
the same block already carries an #else stub returning TEST_SKIPPED; this now
has the same. Verified by preprocessing test_ocsp.c with HTTP client off:
exactly one declaration and one definition survive, no duplicate.

wolfSSL_SetSession() (capital S) is WOLFSSL_LOCAL -- declared in internal.h,
not public API -- so referencing it from tests/api left an undefined reference
in configurations that do not export internal symbols. The two calls are
removed rather than guarded: the public wolfSSL_set_session() is already
exercised a few lines above in the same function and covers the same guard, so
nothing is lost.

Smoke suite: 77 passed, 0 failed.
Letting the run against 15732a8 finish surfaced three problems that the
earlier partial sampling had not, all in tests added this part.

wolfSSL_dtls_set_mtu at test_dtls.c:9337 was guarded on WOLFSSL_DTLS_CH_FRAG
alone. It is declared under (WOLFSSL_SCTP || WOLFSSL_DTLS_MTU) && WOLFSSL_DTLS,
so a config that fragments ClientHellos but has neither MTU macro saw an
implicit declaration. This is the same guard bug already fixed in
test_ssl_cert.c; this second call site was missed then.

df_secret_cb did not match TlsSecretCb. The typedef is
int (*)(WOLFSSL*, void* secret, int secretSz, void* ctx); the callback had an
extra uid=1000(dan) gid=1000(dan) groups=1000(dan),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),103(kvm),105(netdev),110(lpadmin),113(scanner) parameter and a const qualifier, which
-Werror=incompatible-pointer-types rejects wherever HAVE_SECRET_CALLBACK is on.

XSTRNCPY(longHost, "http://", sizeof(longHost)) tripped
-Werror=stringop-truncation. The buffer is filled and terminated by hand on
the next three lines, so the bounded copy bought nothing; it is an XMEMCPY of
the seven-byte prefix now.

Verified with -Werror against a dtls13+cid+mtu config with
HAVE_SECRET_CALLBACK forced on: both touched files compile clean.
…them

The four calls fenced behind WOLFSSL_DTLS_CID_NULL_ARGS_GUARDED crashed an
unpatched library, so they could not run: a segfault discards the coverage of
every test in the variant. origin/master now guards all four --
wolfSSL_dtls_cid_use, _is_enabled and _set check ssl, and _set checks the cid
buffer after its size == 0 early return, so (NULL, 0) still means empty CID --
and the fence and its explanation are no longer needed.

Verified against the rebased tree with the dtls module: builds, runs, and
gates clean with the vectors live. dtls.c 26/56, dtls13.c 70/132, unchanged.
The rebase push took CI from 108 failures to 10. Of those, three are ours.

LeakSanitizer flagged two allocations the tests own and discard:

  wolfSSL_SESSION_dup() returns a new session object, not a borrowed one, so
  calling it for its side effect leaks it -- 2664 bytes from
  wolfSSL_NewSession. The duplicate is freed now.

  wolfSSL_CertManagerNew_ex(NULL) returns an owned CertManager -- 280 bytes.
  It was called bare to exercise the NULL-heap argument; the result is freed
  now.

wolfSSL_SNI_GetRequest and wolfSSL_SNI_GetFromBuffer are compiled under
HAVE_SNI && !NO_WOLFSSL_SERVER (src/ssl_api_ext.c): both read what a client
sent, so a client-only build has neither. Guarding on HAVE_SNI alone left them
undefined at link time there.

Verified with -Werror in a client-only build (NO_WOLFSSL_SERVER, SNI and ALPN
on): both touched files compile clean.

The other seven failures are not ours: scripts/ocsp.test needs external DNS
and the runner had none ("Couldn't find www.google.com, skipping", then
"Both OCSP connection to globalsign and google failed"); that script is
upstream and untouched by this branch. The make-check-linux matrix entries
report "aborted (fail-fast)", i.e. cascade from a sibling, not independent
failures.
The largest remaining category in dtls13.c is not NULL guards but
ssl->options.side comparisons. A connection has one side for its whole life,
so each of those decisions is taken the same way on every call that endpoint
makes, and a test owning both endpoints does not help: MC/DC wants both
outcomes of the SAME decision in one binary's profile. Setting the side by
hand is the only way to pair them.

New driver test_dtls13_role_whitebox.c, 348 vectors over a zeroed WOLFSSL with
its ctx pointed at a client CTX -- these functions read options, keys and
dtls13Rtx and take scalars; none needs a peer or a handshake:

  Dtls13AcceptFragmented   side x type x encryption x ChFrag x dtlsStateful
  Dtls13CheckEpoch         side x type x epoch, over the whole switch
  Dtls13SaveOrFlushClientHello  side x connectState across the range bounds
  Dtls13SetEpochKeys       stored epoch side vs requested side, all nine pairs

dtls13.c 70/132 -> 79/132.

CRL and OCSP get the null guards their callers cannot reach:

  StoreCRL(crl == NULL || path == NULL) -- both operands; every in-tree caller
  validates both before reaching it.

  FreeOcspEntry(entry == NULL || !entry->ownStatus) -- an entry with a borrowed
  status list is what the multi-response path builds, and freeing one must be a
  no-op rather than a double free.

  CheckOcspRequest's ioCtx selection (ssl && ssl->ocspIOCtx != NULL) -- an ssl
  with no per-connection IO context, which falls back to the manager's, is
  produced by no existing test.

crl.c 29/48 -> 31/48, ocsp.c 21/47 -> 24/47. Smoke: 78 drivers, 0 failed.
A census of the remaining NULL-shaped conditions splits them by how the NULL
actually arises: 282 from an argument the caller passes, 145 from a struct
member legitimately NULL in some state, and only 15 from a failed allocation.
This batch takes the first kind in public functions -- no fixture needed at
all, which makes it the cheapest coverage left.

One call per uncovered operand with every other argument valid, then the
all-valid partner: a NULL in the first slot pairs only the first operand
because the rest short-circuit away. Covered here: the two cipher-list getters
(buf/len), check_domain_name and check_ip_address, CTX_GetDevId,
get_cipher_suite_from_name, get_curve_name including its per-curve OID arms,
load_verify_locations_ex's compound (file == NULL && path == NULL),
use_certificate_ASN1, and export_keying_material.

Every symbol was checked against BOTH its ssl.h declaration guard and its
implementation guard in src/ before being called. A declaration without a
compiled implementation is a link error rather than a compile error, and that
distinction has cost this branch several CI rounds.

ssl.c 12/89 -> 24/89, ssl_load.c 35/155 -> 41/155, ssl_certman.c 47 -> 48.
…regression

The rebase cost ssl_sess.c two conditions: 32/120 before, 30/120 after, same
denominator, deterministic across two runs with byte-identical GAPS.md. The
only upstream change to that file is a one-line fopen swap inside
wolfSSL_save_session_cache, whose conditions are all still covered, so the
cause is elsewhere -- most likely the +52 lines upstream added to src/ssl.c,
which drives these paths.

Rather than accept a lower baseline, the two are recovered by covering more:
the session object lifecycle guards, which are almost all
"session == NULL || something about the session" and which a test that
establishes a session reaches with a well-formed object every time.

wolfSSL_SESSION_new / _dup / _up_ref / _free are unguarded in both ssl.h and
src/ssl_sess.c -- checked before writing, since a declaration without a
compiled implementation is a link error, not a compile error. Vectors cover
the NULL half of each entry point, a session that exists but was never
established, the up_ref / double-free refcount path, set_session with a
not-set-up session, and SetServerID's three operands plus its new-session arm.

ssl_sess.c 30/120 -> 32/120. Gate passes with no baseline drop.
Two more batches of public-API argument NULLs, the cheapest category left.

ssl_api_dtls.c 11/53 -> 19/53. Its guards are ordinary NULL-and-zero pairs,
but the accepting half of most needs a DTLS connection, which is why the file
sat at 3/53 until one was supplied. Added: dtls_get0_peer's two operands,
DTLSv1_get_timeout's two, set_timeout_max including the zero boundary,
dtls13_use_quick_timeout with the fast-timeout flag set both ways, the
dtls13_pending_work chain driven through each state it reports on (output
buffered, key update owed, ack owed) because a connection only reaches those
mid-flight between a blocked write and its retry, and SetCookieSecret's
"buffer with zero length", which is neither the clear call (NULL, 0) nor a
real secret.

x509.c 12/37 -> 21/37, reusing the parsed-certificate fixture already in this
file. Added: check_host's object and string operands plus the chklen case
where the length includes the NUL terminator -- what a caller using strlen()
never passes; check_ip_asc's three operands including an unparseable address;
and load_certificate_file's NULL name, an empty file, a directory, and an
unknown format.

Guards were read from src/ssl_api_dtls.c and src/x509.c before writing rather
than assumed from ssl.h. dtls13_pending_work is compiled only under
WOLFSSL_DTLS13, SetCookieSecret only under WOLFSSL_DTLS && !NO_WOLFSSL_SERVER,
several are gated on !WOLFSSL_LEANPSK, and the X509 name checks need !NO_ASN.
…ack cause

The sweep only drove a CTX, one connection object and three file loads, so it
reached few of the error arms it exists for. It now also exercises the
extension setters, the session object lifecycle, the CertManager with its CRL
and OCSP sub-objects, and the chain loader -- each of which allocates on paths
whose failure branches a working configuration never takes.

ssl_certman.c 48/113 -> 50/113, ssl_load.c 41/155 -> 43/155.

The WOLFSSL_SMALL_STACK exclusion stays, but its comment no longer says the
cause is unknown: DecodeCertInternal indexes RPKdataASN before checking the
ret that CALLOC_ASNGETDATA sets, so an allocation failure dereferences NULL
while parsing any certificate. A per-index sweep crashes at five indices
(7, 30, 51, 68, 90), all at the same instruction, reached through
load_verify_locations, use_certificate_file, use_certificate_chain_file and
CertManagerVerify. Fixed upstream in PR 11378; the exclusion comes off once
that merges and the sweep passes on the small-stack variant.
Error propagation is the largest uncovered category in internal.c, and most of
it needs the failing value produced by something upstream. These two functions
are the exception: they classify an error handed to them, so the failing value
is an argument and every arm is reachable by passing the code that arm names.

DoCertFatalAlert maps a verification failure onto the alert the peer is sent.
A handshake produces one failure at a time and most of them not at all -- an
expired certificate, then a path-length-invalid one, then a revoked one, each
needing its own chain -- so the arms are mutually exclusive per run and never
pair. The mapping is security-relevant: it decides what a rejected peer learns
about why. Swept over every code it discriminates on, two it does not, and
both tls1_3 settings, since NO_PEER_CERT branches again on that.

ProcessPeerCertCheckKey enforces the per-algorithm minimum key size. The
minimums are configuration, fixed for the life of a connection, and the
negative sentinel is never set by a working one, so both operands of each
guard are constant in any real run. Swept over each key OID the switch names
plus one it does not, four minimums including the sentinel, and verifyNone
both ways.

75 vectors, no fixture, no fault injection, no certificate chain -- a zeroed
WOLFSSL and a DecodedCert filled in by hand. internal.c 762/1732 -> 780/1732.
Three more error-classification clusters, none needing a peer.

CsrDoStatusVerifyCb lets an application override the library's OCSP verdict,
and the interesting arms are the disagreements: the callback forcing an error
on a good status, and clearing one on a bad status. No in-tree test installs a
callback that disagrees, so neither arm had been taken. A mock callback
returning a chosen value against a chosen incoming result sweeps the matrix,
including the invalid positive return.

DoCertificateStatus compares the declared status length against the record
size; a conforming peer always makes them agree, so the mismatch arms need
bytes no real peer sends. Driven with crafted input, no fixture.

SendData's opening guards ask whether the connection is resuming from a
blocked write, which a test that writes successfully never sets up. The
oversized-length guard above them returns before any IO happens.

internal.c 780/1732 -> 784/1732.
…isions

ProcessPeerCertLeafRevocation decides what a revocation answer MEANS, and its
guards discriminate between specific codes: an explicit assertion that the
certificate is revoked, a responder that does not know it, one that could not
be reached, a certificate naming no responder, a lookup still in flight, and
the CRL equivalents. The difference between them is the difference between
failing a handshake and continuing it. Producing them for real needs four
separate responder deployments, one per vector, so none of these arms had
been taken.

This translation unit already #includes internal.c, so the revocation entry
points it calls but does not define are redirected to fakes with a #define
ahead of the include -- the idiom mcdc_fault_hash.h already uses for wolfcrypt
primitives. CheckCertOCSP_ex, CheckCertCRL, CheckCertCRL_ex and
OcspNoUrlPolicy live in ocsp.c and crl.c, so this rewrites only the driver's
copy and leaves the library untouched. Each fake returns the code the vector
chose, which is the point: the answer is the input under test.

The answers are only half of it. The guards below them also read ocspEnabled,
crlEnabled, crlCheckAll, tls1_3, totalCerts and whether the decoded
certificate has a CA; a first version pinned all six while sweeping only the
codes and gained 2 conditions. Sweeping them one at a time from both
saturated ends, crossed with the codes, gained 13.

7168 vectors. internal.c 786/1732 -> 799/1732.
GetOcspStatus walks the cached status list for a matching serial and then
decides whether the cached answer is still usable. A cache the parser
populated always holds self-consistent entries, so "same length, different
serial", "cached with no stored response body" and "cached with a date that no
longer validates" are states the library only reaches after time passes or a
responder misbehaves. Built by hand: it reads entry->status and writes
*status but stores nothing, so stack objects are correct and the entry is
never linked into ocsp->ocspList.

CheckOcspRequest's remaining guards read what the responder gave back, and a
real responder cannot be asked for a positive length with a NULL buffer, nor
for the two negative sentinels the caller maps onto WANT_READ and
HTTP_TIMEOUT. The mock returns whatever the vector chose, across: a body with
and without a free hook, a NULL buffer with a positive length, a zero-length
reply, a generic error, no transport installed at all, a URL that is present
but empty, and no URL. Each row uses a distinct issuer hash so it misses the
cache and actually reaches the transport.

ocsp.c 24/47 -> 28/47.

Not attempted, and worth recording: CheckOcspResponse's newStatus/newSingle/
ocspResponse NULL checks are WOLFSSL_SMALL_STACK allocations and plain stack
arrays otherwise, so in the default variant the decision cannot be true at all
and in the small-stack variant it needs the allocation injector, which is
fenced until PR 11378 lands.
The signature-verify wrappers in internal.c all end in the same two-operand
guard, and an ordinary handshake pairs neither operand. A good signature gives
(F,F). A bad signature also gives (F,F) at that line, because a bad signature
is not an error: the wc_*_verify_* call returns 0 and reports the verdict in
eccVerifyRes. The first operand is true only when the maths itself breaks.

WOLF_CRYPTO_CB is the supported way to be the thing that breaks. mcdc_fault_
cryptocb.h registers a device that answers CRYPTOCB_UNAVAILABLE to everything
except the one operation a vector selects, so each wrapper can be driven three
ways: the device refuses, the device succeeds with the verdict "no", the
device succeeds with the verdict "yes". Dispatch happens after the argument
checks but before any key material is touched, so the vectors need a key
object carrying a devId and nothing else -- no certificate, no peer, no valid
public point. wc_ed448_verify_msg zeroes *res before dispatching and the other
two do not, so the device sets the verdict rather than the caller.

VerifyRsaSign's recovered-plaintext check uses the #define idiom instead, and
the reason is worth keeping. RsaPublicDecrypt routes every operation except
verify through the callback, and the path that does dispatch feeds its output
back through PKCS#1 unpadding, so a device returning anything but a correctly
padded block makes ret negative and the guard is never reached. The middle
operand also defends against a positive length arriving with a NULL buffer,
which no implementation produces. Redirecting the call reaches all three.

internal.c 799/1748 -> 810/1748.

One vector failed and is kept for what it shows. EccMakeKey's (ret == 0 &&
key->dp) looked like the same shape -- let a device claim success without
generating a key and dp should still be NULL. It is not: _ecc_make_key_ex
calls wc_ecc_set_curve before it consults the device, and set_curve either
fails, making ret non-zero, or assigns dp. (T,F) does not exist, so the
operand is now an exclusion with the argument written out rather than an open
condition.
The ClientHello builder wrote the renegotiation_info extension type as a
conditional whose two branches were the same constant, which is a constant
expression some -Wextra builds reject and which hid what 0xFF01 is. It is
TLSX_RENEGOTIATION_INFO; use that.

The CRL and OCSP skip stubs are guarded on their feature macro plus certs plus
not-WOLFCRYPT_ONLY. Their printed messages already said so; the #else comments
still named only the feature macro, so a reader chasing a skip saw the wrong
reason.

No coverage change: tls_core 810/1748 and revocation 28/47 + 31/48 both
re-measured identical, gates green.
Five configurations failed, four of them at build time, each the same mistake
in a different place: a guard that describes what the test needs rather than
what the build actually provides.

Also registers the four tests/unit-mcdc files that were missing from
EXTRA_DIST.

Verified by building all five configurations locally: all pass. Campaign
unaffected -- dtls, ssl_api and tls_core re-measured identical, gates green,
white-box smoke 82 passed 0 failed.
The upstream hardening commits added defensive checks on deserialized private
key state, and none of them are reachable from a keygen/sign/verify cycle: the
library's own output always satisfies them, so every one of the decisions is
permanently false in an ordinary run. Each is driven here by handing the
function the state a tampered or truncated key would produce.

  wc_lms_priv_state_load       stack offset past the end of the stack, and an
                               offset that is in range but not a whole number
                               of nodes, plus both accepting partners
  wc_lms_treehash_update       a restored offset that says the data stack is
                               already full, so the first push has nowhere to
                               go
  wc_xmss_bds_update           a NULL height array, and an offset past the
                               subtree height
  wc_xmss_bds_next_idx         a retain index below the first retained node,
                               and one that would run off the end of retain

The retain guard needs the merge loop to reach a height at or above
sub_h - bds_k, so bds_k = 3 against sub_h = 4 puts that at height 1 and the
caller-supplied height/offset pair steers the loop there in two iterations.
The three indices then select each operand: 4 gives (i >> h) = 2, 6 gives a
retain offset inside the buffer, 18 gives one exactly at its end.

wc_lms_impl.c 134/140 -> 137/140, wc_xmss_impl.c 71/79 -> 75/79.

The LMS drivers belong inside WB_GAP_SIGN. Placed outside it first, they
failed to compile under WOLFSSL_WC_LMS_SMALL and WOLFSSL_LMS_VERIFY_ONLY, and
because a white-box that does not build is recorded as a skip, both variants
were dropped whole and took four conditions elsewhere in the file with them --
the file went down by one overall while the intended three were covered. Both
drivers are now syntax-checked against every combination of the small and
verify-only macros.

Two conditions are left and neither is this shape: wc_lms_impl.c:2454's ret
operand needs a hash failure earlier in the same loop iteration, which belongs
to the hash-fault driver, and wc_xmss_impl.c:3077 needs a tree-hash instance
that is in use while the stack offset is zero.
    while (signers && ret == NULL)

is false in its second operand only when a match was just assigned and the
bucket still holds an entry after it, so the loop condition is evaluated once
more with ret set. If the match is the only entry in its row, or the last one,
signers goes NULL and the first operand ends the loop instead.

That makes the operand a property of CA-table occupancy rather than of any
test: it needs two CAs hashing to the same row and a lookup for the one that
is not at the tail. Which certificates a run loads, and in what order they
were added, decide whether that ever happens -- which is why this one
condition was covered on one host and not on another from the same tree, the
same tests and byte-identical certificates. The sweep measured 49/113 two
nights running where the development host measured 50/113, and the difference
was this line and nothing else.

The bucket is now built rather than hoped for. GetCAByName reads only
subjectNameHash and next and takes cm->caLock, so two zeroed Signers linked
head to tail are a complete fixture; they are on the stack, so the row is
detached before the CertManager is freed. Four vectors: the head of a
two-entry row, its tail, an absent hash, and a NULL manager.

ssl_certman.c is #included into ssl.c and refuses to compile alone, so the
white-box includes src/ssl.c.
Three more configurations failed to link, all the same shape as the last
round: a guard that names what the test needs rather than what the build
provides.

  NO_TLS builds (certgen-no-tls, no-tls-cryptocb-aesgcm-setkey-free)
      wolfSSLv23_client_method and wolfSSLv23_server_method are implemented
      under !NO_TLS && !NO_WOLFSSL_{CLIENT,SERVER}, and wolfSSL_UseSNI,
      wolfSSL_CTX_UseSNI, wolfSSL_SNI_Get*, wolfSSL_UseSupportedCurve and
      wolfSSL_CTX_UseSupportedCurve all sit under !NO_TLS in ssl_api_ext.c on
      top of their own feature macro. Eleven blocks were missing !NO_TLS.

  dtls13-client-minimal (WOLFSSL_NO_TLS12)
      wolfDTLSv1_2_{client,server}_method are implemented under
      !WOLFSSL_NO_TLS12: DTLS 1.2 is built on the TLS 1.2 code. Three blocks
      wanted that, including the CID argument-guard test.

Found the first pass by line number and missed a second block carrying the
identical guard text, so this was checked mechanically instead: for every call
site of each of these symbols, walk the enclosing #if chain and assert it
carries the macros the implementation requires. That found the leftovers and
now reports zero for this branch. Verified by building all seven affected
configurations locally -- the three new ones and the four fixed last round, so
neither set regressed the other.

Campaign unaffected: ssl_api, dtls, revocation and tls_core re-measured, gates
green, white-box smoke 82 passed 0 failed.
Rebasing onto master put upstream's new verify-mode tests in the same place as
this branch's, and git aligned the two on the shared create-ctx / free-ctx
boilerplate, which splits both function bodies. Resolving those hunks by
copying whole functions across is right, but it moved the mock without the two
file-scope counters above it, so g_crlIoCalls and g_crlIoResult became
undeclared. They are back, immediately above the mock that uses them.

Nothing else was lost: every file-scope static that existed before the rebase
and is still referenced is still defined.
test_ssl_certman_whitebox includes src/ssl.c, and against the previous smoke
build that failed to link: the harness compiles with fixed flags, and whether
XFDOPEN is defined decides whether wc_fopen_owner_only is a function or a
macro. It builds and passes against the post-rebase --enable-all
--enable-static tree, so it is in the expected list now and a break in it will
be caught locally rather than only by a full sweep.
The rebase resolution copied test_crl_io_mock across without the #if that had
surrounded it, leaving it defined at file scope while its only caller keeps
the guard inside its own body. Any build where that body is compiled out --
a default build has neither HAVE_CRL nor HAVE_CRL_IO -- then has a static
function nothing references, which -Werror=unused-function rejects.

The guard is back, and it now carries the !defined(NO_TLS) the caller's body
gained after the original was written, so the two conditions are identical
rather than merely similar.

Reproduced before fixing: the pre-fix file fails to compile in a default build
with exactly "'test_crl_io_mock' defined but not used", and builds clean after.
Checked the other six functions the rebase moved in this file the same way --
comparing each one's enclosing guard chain against its pre-rebase version --
and this was the only one that lost anything.

Verified across default, --enable-crl, --enable-all and a no-TLS build, all
with -Werror=unused-function and -Werror=unused-variable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants