Skip to content

Fix memory-safety, zeroization, and error-handling findings - #1136

Open
ejohnstown wants to merge 15 commits into
wolfSSL:masterfrom
ejohnstown:sf13
Open

Fix memory-safety, zeroization, and error-handling findings#1136
ejohnstown wants to merge 15 commits into
wolfSSL:masterfrom
ejohnstown:sf13

Conversation

@ejohnstown

Copy link
Copy Markdown
Contributor
  • F-7204 - echoserver's LoadPubKeyList() now checks the wolfSSH_ReadKey_buffer(), wc_CertPemToDer(), and WMALLOC() results so a malformed or missing key file skips the entry instead of handing PwMapNew() a NULL buffer and an indeterminate length.
  • F-7205 - DoSshPubKey() wrote its null terminator at c[inSz-1], clobbering the last byte of the copied key; it now writes at c[inSz].
  • F-7206 - _CertMan_init() never stored heap on the struct, so every WMALLOC/WFREE in certman.c ran with a NULL heap; it is now set during init.
  • F-7207 - portfwd_worker() dropped the unsent tail after a short wolfSSH_ChannelSend(), and mistook a full buffer's zero-length recv() for the peer closing; both are fixed.
  • F-7208 - CheckProfile() ignored the wc_GetDateAsCalendarTime() return and compared an indeterminate struct tm; it now zero-initializes, checks the return, and fails the profile on a parse error.
  • F-7209 - DoPemKey() hardcoded isPrivate = 1 when identifying the decoded key; it now passes the caller's flag through.
  • F-7210, F-7211 - the example client tracks public-key allocations with a userPublicKeyAlloc flag rather than pubKeyName (which leaked the cert buffer for -J without -j), and frees the CA cert and the TPM public key from the heap they were allocated from.
  • F-7212 - wolfSSH_MakeRsaKey(), MakeEcdsaKey(), and MakeEd25519Key() freed their key structs unconditionally, so a failed init freed an indeterminate stack struct; frees are now gated on an init flag.
  • F-7213 - GetConfigInt() ran WSTRCMP() on the caller's non-terminated slice, rejecting valid zero values like LoginGraceTime 0m; it now compares the NUL-terminated copy that atol() was given.
  • F-7214 - wolfSSH_AGENT_worker() dereferenced ssh->agent without a null check; it now returns WS_AGENT_NULL_E.
  • F-7215 - wolfSSH_SFTP_DoName() left ret holding the read length, so a NAME response with zero entries took the error path; ret is cleared before the loop.
  • F-7216 - AeadIncrementExpIv() returned early on the first non-zero byte, making the nonce increment data-dependent; the carry now runs the full width with no early exit.
  • F-7219 - SshResourceFree() wiped only kSz bytes of ssh->k, which can under-cover after a rekey; it now zeroes sizeof(ssh->k) and resets kSz.
  • F-7220 - CheckPasswordHashUnix() left the hashed password in crypt()'s static buffer; it is wiped once the comparison is done.

Copilot AI review requested due to automatic review settings July 30, 2026 23:00

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ejohnstown ejohnstown changed the title Fix static analysis issues Fix memory-safety, zeroization, and error-handling findings Jul 30, 2026

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #1136

Scan targets checked: wolfssh-bugs, wolfssh-src

No new issues found in the changed files. ✅

@philljj
philljj self-requested a review July 31, 2026 18:35
@philljj philljj self-assigned this Jul 31, 2026
@philljj
philljj requested a review from Copilot July 31, 2026 19:11

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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Suppressed comments (2)

tests/api.c:1441

  • wolfSSH_ReadKey_buffer() allocates SSH-format key outputs with DYNTYPE_PRIVKEY (via DoSshPubKey), but this test frees them with DYNTYPE_FILE. With memory tracking enabled, freeing with the wrong dyn type can break accounting/debug checks.
    WFREE(keyRef, NULL, DYNTYPE_FILE);
    WFREE(keyTrim, NULL, DYNTYPE_FILE);

examples/portfwd/portfwd.c:714

  • When appBufferUsed == appBufferSz, appFd remains in the read fdset, so select() will return immediately as long as the socket stays readable. Because the recv() is skipped in that state, the loop can busy-spin at 100% CPU while waiting for wolfSSH_ChannelSend() to drain the buffer. Consider clearing appFd from rxFds while the buffer is full and still running the flush logic on select() timeout.
            }
        /* Skip the read when the buffer is full, a zero length recv() returns
         * 0 and would be mistaken for the peer closing. The buffer drains at
         * the bottom of the loop. */
        if (appFdSet && appBufferUsed < appBufferSz &&

@philljj philljj left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good, just 1 question.

Also, copilot suppressed this comment, but I can't figure out if actually valid:

When appBufferUsed == appBufferSz, appFd remains in the read fdset, so select() will return immediately as long as the socket stays readable. Because the recv() is skipped in that state, the loop can busy-spin at 100% CPU while waiting for wolfSSH_ChannelSend() to drain the buffer. Consider clearing appFd from rxFds while the buffer is full and still running the flush logic on select() timeout.

Comment thread tests/api.c Outdated
@philljj philljj assigned ejohnstown and unassigned JacobBarthelmeh and philljj Jul 31, 2026
kSz can change (e.g. after rekeying), so it may no longer cover the
full extent of previously stored key material. Use sizeof(ssh->k)
instead to ensure the whole buffer is wiped, and reset kSz to 0.

Issue: F-7219
heap was used throughout certman.c (WMALLOC/WFREE calls, and the
free at wolfSSH_CERTMAN_free) but was never stored on the struct
during _CertMan_init, so it was always NULL.

Issue: F-7206
- wolfSSH_ChannelSend() can return fewer bytes than requested, but
  appBufferUsed was decremented without moving the unsent tail to the
  front of appBuffer, corrupting subsequent sends. Shift the remaining
  data down with WMEMMOVE() when the send is short.
- Skip the recv() when appBuffer is full. The length argument would be
  0, and a zero length recv() returns 0, which the loop read as the
  peer closing; the buffered data was dropped and the forward torn
  down.

Issue: F-7207
c[inSz-1] = 0 clobbered the last byte of the copied key data instead
of terminating the string after it, truncating public keys by one
character.

Issue: F-7205
- Test wolfSSH_ReadKey_buffer() and wc_CertPemToDer() results and skip
  the entry; a malformed file gave PwMapNew() a NULL buf and an
  indeterminate length.
- NULL-check both WMALLOC() results, and guard the load_file() size so
  a missing file no longer reaches WMALLOC(0).
- Fall through to the loop's existing WFREE()/advance so both buffers
  are freed on every path.

Issue: F-7204
- Zero-initialize struct tm t; it was read indeterminate when the parse
  failed.
- Fail the profile when wc_GetDateAsCalendarTime() returns non-zero, and
  gate the two date-format comparisons on valid.

Issue: F-7208
- IdentifyAsn1Key() was called with a literal 1, so a public PEM decoded
  by wc_PubKeyPemToDer() was run through the private-key decoders and
  never identified.
- Add test_wolfSSH_ReadPublicKey_pem() covering a public RSA PEM read
  through wolfSSH_ReadPublicKey_buffer(). The read itself only compiles
  with WOLFSSH_TPM, so the test also asserts the isPrivate 0 vs 1
  difference in IdentifyAsn1Key() directly, which every build runs.

Issue: F-7209
- Track userPublicKey allocations with a userPublicKeyAlloc flag
- Free on that flag, not pubKeyName; -J with no -j leaked the cert
- Restore userPublicKeyBuf when a key load fails, not a stale pointer
- Tag load_der_file() allocations DYNTYPE_PRIVKEY to match the frees
- Free the CA cert in ClientLoadCA() with the heap it came from
- Pass the caller's heap into wolfSSH_TPM_InitKey() so the TPM public
  key is allocated from the pool ClientFreeBuffers() frees it with

Issue: F-7210, F-7211
- crypt() returns a pointer into a static buffer that keeps the hashed
  password after CheckPasswordHashUnix() returns; wipe it once the
  comparison is done.

Issue: F-7220
- The zero check ran WSTRCMP() on the caller's buffer, which is a
  length-bounded slice of the config line and not NUL terminated, so it
  read past inSz and rejected valid "0" values whose slice had trailing
  text.
- Compare num, the NUL-terminated copy that atol() was given.

Issue: F-7213
- wolfSSH_MakeRsaKey and wolfSSH_MakeEcdsaKey called wc_FreeRsaKey and
  wc_ecc_free unconditionally, so a failed init left the free operating
  on an indeterminate stack struct.
- Track init with a keyInit flag, as wolfSSH_MakeMlDsaKey does, and free
  only when the init succeeded.
- Guard wc_ed25519_free in wolfSSH_MakeEd25519Key the same way.
- Replace the tab indent on the ed25519 free with spaces.

Issue: F-7212
- wolfSSH_AGENT_worker() dereferenced ssh->agent to set the DONE state
  without checking it, so a call before the agent was set up took a null
  dereference.
- Return WS_AGENT_NULL_E instead.

Issue: F-7214
- ret held the read length from wolfSSH_SFTP_buffer_read(), and was only
  reset at the bottom of the per-entry loop, so a NAME response with a
  count of 0 skipped the loop and logged a read error.
- Set ret to WS_SUCCESS after the count is parsed. The NULL return and
  untouched ssh->error are unchanged; only the bogus log goes away.

Issue: F-7215
- AeadIncrementExpIv() returned as soon as a byte incremented without
  wrapping, so the iteration count depended on the counter, which starts
  out as KDF output. Carry through all 8 bytes instead.
- Same counter values and the same instruction count once unrolled; the
  built object no longer has the data-dependent exits.

Issue: F-7216
The send at the end of the loop only ran when select() reported a
readable descriptor, but appBuffer can hold data across iterations.
Mask appFd out of the read set while the buffer is full, and fall
through the select() timeout to the send.

Issue: F-7207
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.

5 participants