With WOLFSSL_NO_MALLOC there is no allocator behind a NULL-heap XMALLOC,
so an allocation made outside any CTX or SSL object can only be served
from the global heap hint. wolfSSL_Init() makes such an allocation: under
OPENSSL_EXTRA it seeds the compatibility-layer RNG, whose _InitRng() call
allocates with a NULL heap. That returned NULL, wolfSSL_Init() reported
WC_INIT_E, and every wolfSSL_CTX_new_ex() that triggered the lazy init
failed, so neither example could establish a connection even though the
pool it had loaded was large enough.
Have each example nominate its own pool, which is what the hint is for.
The server could not do that before: it created its CTX through
wolfSSL_CTX_load_static_memory(), which loads the pool and creates the CTX
in one call, so wolfSSL_Init() ran before the caller ever saw the hint.
Load the pool with wc_LoadStaticMemory() and create the CTX afterwards, as
the client already does.
Claim the hint only when none is set, and drop it again on the way out.
Both pools are local to the example's own function, and testsuite runs the
wolfCrypt test, both examples and the echo server in one process, so an
example that overwrote the hint would leave it pointing at a pool that
dies the moment the example returns.
scripts/resume.test, scripts/tls13.test and testsuite/testsuite.test go
from failing to passing with --enable-staticmemory -DWOLFSSL_NO_MALLOC.
Restrict the claim to the standalone programs (!NO_MAIN_DRIVER). testsuite
and unit.test compile both examples with NO_MAIN_DRIVER and run server_test
on a spawned thread beside client_test, so an in-harness claim would publish
one thread's automatic-storage pool as the process allocator, let the other
thread allocate from it, and then revoke it when the owning frame unwound.
In those builds the harness's own long-lived pool is the one that belongs in
the hint.
Check ctx after wolfSSL_CTX_new_ex() rather than letting the following IO-pool
load report a CTX allocation failure as "unable to load static memory".
With WOLFSSL_NO_MALLOC there is no system heap to fall back on, so an
allocation the compatibility layer makes with a NULL heap has to come out
of the static pool the test loads. The test only nominated that pool as
the global heap hint when OPENSSL_EXTRA was off, so in the combination of
the two those allocations had no source at all and openssl_test() failed.
Set the hint in that combination too, and give it a pool sized for the
compatibility layer on top of the algorithm tests. gTestMemory was sized
for the wolfCrypt tests alone and those allocations exhausted it - first in
wolfSSL_CRYPTO_malloc(), then, as the pool was enlarged, further along in
wolfSSL_X509_load_certificate_file(). Every other configuration keeps the
size it had.
The new arm goes ahead of the FrodoKEM and ML-DSA arms rather than after
them, because those fire first for exactly the builds that need the most.
With --enable-mldsa the 576 KB arm won and the RSA test died with -125, and
1 MB only moved the failure to openssl_pkey1_test(), where an exhausted pool
surfaces as a NULL from wolfSSL_X509_get_pubkey() and no error code at all.
FrodoKEM fails the same way on its own 1 MB arm. Both pass at 2 MB, so the
arm asks for 2 MB when either is enabled and 1 MB otherwise.
Claim the hint only when it is unset, and drop it only while it is still
ours. Nothing in tree installs one before wolfcrypt_test() runs, but under
OPENSSL_EXTRA the hint is never handed back, so a program embedding
wolfcrypt_test() as a smoke test would otherwise lose its own pool for the
rest of the process. This is the first-wins rule the examples already use.
The matching teardown stays restricted to !OPENSSL_EXTRA on purpose.
gTestMemory has static storage duration, so the hint cannot dangle, and
testsuite runs the echo server after wolfcrypt_test() returns: the echo
server has no pool of its own, and this is the one that outlives it.
testsuite/testsuite.test now passes with --enable-staticmemory
-DWOLFSSL_NO_MALLOC. scripts/unit.test still fails there, at seven API
tests this change does not address.
BENCH_EMBEDDED gets a #error rather than the enlarged pool. That combination
is self-contradictory - openssl_pkey0_test() is not gated on BENCH_EMBEDDED, so
it still asks for about 1 MB, which is not something to hand a target that has
declared itself embedded. Refusing at build time with the override named beats
either silently reserving a megabyte or failing at run time in RSA_new(), which
is what a 14 KB pool does today. WOLFSSL_STATIC_MEMORY_TEST_SZ remains the
override and is checked first, so such a target can still pick its own size.
The arm is also restricted to !WOLFCRYPT_ONLY. The compatibility layer tests
are gated on !WOLFCRYPT_ONLY themselves, so a crypt-only build never runs
openssl_pkey0_test() and has no reason to reserve a megabyte for it - and
with BENCH_EMBEDDED it met the #error above over a test it does not compile.
Crypt-only keeps the size it had.
Pin the behaviour the asn.c fix restores while it is here: cert_no_malloc_test()
only asserted the in-place layout under WC_ASN_NO_HEAP, so nothing covered the
copy-out side. Add the mirror assertion - pubKeyStored set, publicKey outside
the source DER. It fails on the pre-fix guard and passes after it, and unlike
fill_signer_twice_test() it is not gated on NO_FILESYSTEM, which a real
static-memory target turns off. Carry ParseCert()'s whole predicate rather
than WC_ASN_NO_HEAP alone: the copy-out is also skipped under
NO_WOLFSSL_CM_VERIFY without WOLFSSL_DYN_CERT, and the assertion must not
claim otherwise there.
The same goes for NO_RSA and NO_SHA. Both openssl_pkey0_test() and
openssl_pkey1_test() compile their bodies away without either one, and
openssl_test() alone then fits the sizes the old ladder gives it:
--disable-rsa with BENCH_EMBEDDED passes the whole suite on the 14000-byte
arm, and without BENCH_EMBEDDED it passes on the 160000-byte arm rather
than reserving a megabyte. Excluding the arm reaches both of those;
narrowing only the #error would have left the plain build at 1 MB and
handed the embedded one the same megabyte the #error exists to refuse.
ParseCert() guarded its RSA public-key copy with !WOLFSSL_NO_MALLOC,
while StoreKey() guards the equivalent copy for every non-RSA key with
!WC_ASN_NO_HEAP. Those are not the same condition: WC_ASN_NO_HEAP is
auto-defined only when WOLFSSL_NO_MALLOC and NO_WOLFSSL_MEMORY are set
without XMALLOC_USER or WOLFSSL_STATIC_MEMORY, so a static-memory build
defines WOLFSSL_NO_MALLOC yet still has a working allocator.
In such a build the copy was skipped, cert->pubKeyStored stayed 0, and
FillSigner() therefore never populated signer->publicKey/pubKeySize.
ParseCertRelative() then passed a NULL key and a zero key size to
ConfirmSignature(), which rejects them with BAD_FUNC_ARG before its
WOLFSSL_ENTER. The effect was that no certificate issued by an RSA CA
could be verified against it - wolfSSL_CertManagerVerifyBuffer() and TLS
peer validation alike - while ECC, Ed25519, Ed448 and ML-DSA CAs worked,
because those keys travel through StoreKey().
Use WC_ASN_NO_HEAP in all three guards, including the one on the ptr
declaration. FreeDecodedCert() and FreeSigner() already key off
pubKeyStored, so ownership and freeing are unchanged.
Point the MC/DC white-box guard for this block at WC_ASN_NO_HEAP too. It
still keyed off WOLFSSL_NO_MALLOC, so in a static-memory build the copy is
now compiled and executed while the section covering it fell back to its
stub, and the coverage claim was inaccurate for the one configuration this
fixes.
* CI: resolve the ghcr .deb bundle against its own apt index
The bundle was resolved on master right after apt-get update, against the
live archive; consumers resolved against the apt lists frozen into the
runner image days earlier. Any version published in between made
--no-download ask for a .deb the bundle did not carry, and the
all-or-nothing install sent the whole set to the mirror. 25% of PR jobs
took that path, and in 16 of 20 sampled cases the mirror then supplied
zero bytes - the bundle was complete, only the index disagreed.
ci-deps-image now ships a dpkg-scanpackages index in each bundle and
install-apt-deps resolves against only that, as a local file:// repository
with its own lists dir. Producer and consumer agree by construction, so
only a package genuinely absent from the bundle falls back.
Also:
- rebuild the static bundles daily and on a merged package-list change,
instead of weekly
- pq-all gets its own -cross bundle; crossbuild-essential-* were in no
24.04 list, so it fetched 118 MB from the mirror on every run
- ccache-setup installs from the repository install-apt-deps exported,
rather than from debs staged in /var/cache/apt/archives
- check-ci-deps.py fails a PR whose install-apt-deps call names a package
its bundle does not carry, a tag that does not exist, or a tag for the
wrong Ubuntu release; wired into check-source-text
- ci-deps-canary asserts the same contract dynamically after every
rebuild, with require-bundle turning a fallback into an error
- whitebox-smoke used a bare apt-get; it now uses the bundle
* Address review: fail check-ci-deps.py in --matrix/--sets mode
Static contract violations printed ::error but exited 0, so ci-deps-canary
could not fail on them. Those modes also hand stdout to their caller as
data, so findings went to stderr and would have corrupted it.
Route findings to stderr in the two data modes, return non-zero when any
were found, and read --sets output from a file in the canary so the exit
status is not swallowed by process substitution.
* CI: make the .deb bundles reach container jobs
sssd.yml runs in a container, and install-apt-deps skipped the bundle
there for two reasons, both of which left it on the apt mirror - where a
slow archive killed apt-get update twice and failed the job.
1. No docker CLI inside a job container, so the bundle was never pulled.
ghcr-pull.sh pulls it from the registry with curl and tar instead.
2. A bundle is a closure relative to what was already installed where it
was resolved. -full is resolved on the runner, which already has bc and
libcap2, so neither .deb was in it and the all-or-nothing offline
install could never succeed inside the container. ci-deps-image now
takes an `image` per matrix entry and resolves that bundle inside it;
sssd.yml gets its own ubuntu-24.04-sssd bundle, a handful of .debs
rather than 480 MB.
check-ci-deps.py holds the two sides together - a container job must name
a bundle built in its own image, and no other job may name it - and
ci-deps-canary proves each image-tied bundle inside that image after every
rebuild. install-apt-deps also checks the bundle's release against
/etc/os-release, which the static check cannot see for a container job.
Also fix the apt fallback's budget split, which is what actually failed
the job: update was capped at a sixth of an attempt, 50s, well under the
~90s apt's own Acquire retries need to get past a stalled mirror. It is
now half an attempt capped at 90s, taken from the budget still unspent,
so the loop uses the 600s it was given instead of giving up after 105s.
* Address review: record the image the closure was resolved in
bundle-info reported ${ImageOS}/${ImageVersion}, which is the runner's
OS even for a matrix entry whose closure is resolved inside a container.
Report the container image for those entries and keep the runner on its
own line.
Two PRB findings, unrelated to each other.
tests/unit-mcdc/test_ssl_certman_whitebox.c and test_ssl_sess_whitebox.c were
never added to EXTRA_DIST, so make dist left them out. Confirmed by building a
tarball before and after: the two were the only files this branch adds that
were missing, and both are in it now. Every other file the branch adds was
already listed.
test_wolfSSL_ocsp_stapling_accessors wrote cssl->ocspProducedDateFormat
directly. Expect* records a failure and carries on rather than returning, so
on the path where wolfSSL_new() failed that is a dereference of NULL, which is
what the static analyser reported at the ExpectNotNull above it. The three
direct field writes are guarded; the calls that merely pass cssl are safe
either way, because the API checks it.
Checked the rest of the file for the same shape rather than fixing only the
reported line: of the raw dereferences of Expect-obtained pointers, the DTLS
ones were already inside if (dssl != NULL) blocks and the remainder are inside
Expect* macros, which short-circuit once a previous one has failed. These
three were the only unguarded ones.
Both session caches walk their row backwards from the most recently used
entry, and both start the walk with
idx = row->nextIdx - 1;
if (idx < 0 || idx >= SESSIONS_PER_ROW)
nextIdx is the ring's insertion point, so idx lands in [-1, PER_ROW-1] and the
first operand is true exactly when nextIdx is 0 -- an untouched row, or one
that has just wrapped. Whether any test produced that state was decided by
what an earlier test in the same binary had left in the cache, not by anything
the test itself did. That is not hypothetical: ssl_sess.c measured 32/120 on
2026-09-05 and 33/120 on 2026-09-06 from identical wolfssl and campaign
commits on the same host, and 33 is what this now reaches every run.
The white-box empties the rows itself and looks up against them, then repeats
each lookup with nextIdx in the middle of the ring, because both halves have
to run in one binary or the operand has no independence pair -- a first
attempt drove only the true side and scored zero.
Two details cost a measurement each and are worth writing down: ClientCache is
CLIENT_SESSION_ROWS long while SessionCache is SESSION_ROWS, so zeroing the
first with the second's bound leaves the row the id hashes to untouched; and
wolfSSL_GetSessionClient returns before the ring walk when the context has the
cache switched off, which makes every vector a silent no-op.
ssl_sess.c 32/120 -> 33/120. The second operand of each guard is excluded
rather than chased: nextIdx is bounded by the ring at every write, so
idx >= PER_ROW cannot occur and has no pair in any configuration.
tests/api is one binary compiled in every CI configuration, so a test calling
an API the build did not compile is not a test failure -- it is a link error
that takes the whole binary down. It is also invisible to header inspection,
because wolfSSL declares plenty of API unconditionally and implements it under
a narrower condition. That combination broke CI four separate times on this
branch, each time found by CI rather than locally, and each time the fix was
the same: name what the build provides, not what the test needs.
check-api-guards.py walks the enclosing #if chain of every call site and
requires the macros the IMPLEMENTATION carries. It is a whitelist rather than
a parse of ssl.h on purpose: the mapping from symbol to implementation guard
cannot be derived from the declaration, which is the whole problem.
Two things make it usable rather than noisy:
It only looks at call sites this branch changed. Run over everything it
reports 28 long-standing sites that are fine in practice because the
configurations that would break them are not built; auditing those is a
different job, and --all still does it.
It knows which macros imply TLS. A block under WOLFSSL_TLS13 or HAVE_SNI
cannot also need !defined(NO_TLS) spelled out, and comments and string
literals are blanked before matching, since these files discuss the very
API names being searched for.
It refuses to run against a ref it cannot resolve rather than reporting
success, because a shallow checkout would otherwise make every diff empty and
the check would pass without looking at anything. The workflow checks out with
fetch-depth: 0 for that reason, and runs the check before the smoke build --
it needs no build and costs a second.
Verified both directions: clean on this branch, and it reports the exact site
when !defined(NO_TLS) is removed from a guard that needs it.
The sweep was excluded from WOLFSSL_SMALL_STACK because DecodeCertInternal
indexed RPKdataASN before checking the ret that CALLOC_ASNGETDATA sets, so
failing an allocation dereferenced NULL while parsing any certificate. That
was reported and fixed upstream in PR 11378, which the last rebase brought in,
so the exclusion is gone.
Verified rather than assumed: built --enable-all --enable-smallstack with
WOLFSSL_SMALL_STACK actually defined in options.h, ran the ssl_cert group, and
the sweep passes with no errors and no crash. A crash would have discarded the
whole variant, which is what the exclusion was protecting against.
The small-stack variant now contributes what it always should have:
internal.c 810/1748 -> 825/1754
ssl_load.c 43/182 -> 48/182
ssl_certman.c 50/113 -> 53/113
keys.c 33/40 -> 34/40
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.
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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.
Letting the run against 15732a80d 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.
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.