Skip to content

CDRIVER-3228 fix memory leaks in SChannel cert loading #2009

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
May 16, 2025

Conversation

kevinAlbs
Copy link
Collaborator

Summary

This PR addresses several memory-related found in certificate loading for Secure Channel:

  • Fix several memory leaks.
  • NUL-terminate the PEM file for the client certificate.

Reading the PEM file is refactored with more error checks logs including the system error.

Background

Leaks were discovered when implementing PKCS#8 certificate support for (CDRIVER-4269). This may help motivate future addition of leak detection on Windows (CDRIVER-2529).

NUL-terminating the PEM file was similarly done in #1903. NUL-terminating is intended to avoid reading past beyond the string in later calls to strstr.

The Secure Channel implementation would likely benefit from a larger rewrite, and may be done as part of CDRIVER-4463. This PR intends to address the more severe issues and I expect can be backported with low risk of unexpected behavior change.

@kevinAlbs kevinAlbs force-pushed the leaks-only.C4296 branch from 1a0bce5 to 61e39c5 Compare May 9, 2025 14:15
@kevinAlbs kevinAlbs marked this pull request as ready for review May 9, 2025 16:31
@kevinAlbs kevinAlbs requested a review from a team as a code owner May 9, 2025 16:31
@kevinAlbs kevinAlbs requested review from rcsanchez97, eramongodb and a user and removed request for rcsanchez97 May 9, 2025 16:31
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Nice improvements in a pretty hairy area of code. It's always fun litigating subtleties of under-documented legacy APIs :)

I had a handful of comments but I don't think any of them should be treated as blockers. The type confusion possible from CERT_QUERY_CONTENT_FLAG_ALL might be my biggest concern but it's not new.

I think it's possible to simplify the changes to tls-secure-channel (freeing 'cert' early instead of storing it) but I haven't verified this beyond reading docs and code.

Comment on lines 81 to 90
MONGOC_ERROR ("Failed to get length of file: '%s'. File too large", filename);
goto fail;
}

if (0 != fseek (file, 0, SEEK_SET)) {
MONGOC_ERROR ("Failed to seek in file: '%s' with error: '%s'",
filename,
bson_strerror_r (errno, errmsg_buf, sizeof errmsg_buf));
goto fail;
}
Copy link

Choose a reason for hiding this comment

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

Just a thought, if we wanted to simplify the error reporting here, some of these are definitely worth catching at runtime but maybe not worth having a unique error string for. (I'm thinking that the LONG_MAX-1 check and the fseek to zero check are needed for security, but in practice will only fail due to unusual filesystems.)

Copy link

Choose a reason for hiding this comment

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

I'm also noticing that below we have a possible overflow when the strlen of an individual key in the file is cast to DWORD (see public_blob.cbData). The security implication seems acceptably minor there, but it'd be a small improvement IMO to disallow file sizes larger than INT_MAX or so and remove that bit of attack surface.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not worth having a unique error string for

Agreed. Removed error log for the unlikely-to-encounter errors.

possible overflow when the strlen of an individual key in the file is cast to DWORD

Good spot. This is addressed by replacing the call of CryptQueryObject with CertCreateCertificateContext. Calculating the length is no longer needed.


/* https://msdn.microsoft.com/en-us/library/windows/desktop/aa376573%28v=vs.85%29.aspx
*/
// CertSetCertificateContextProperty takes ownership of `provider`.
Copy link

Choose a reason for hiding this comment

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

This comment is very helpful! I'm noticing that the specific doc we could reference is on the CERT_KEY_PROV_HANDLE_PROP_ID property rather than the whole of CertSetCertificateContextProperty:

CERT_KEY_PROV_HANDLE_PROP_ID

Data type of pvData: A HCRYPTPROV value.

The HCRYPTPROV handle for the certificate's private key is set. The hCryptProv member of the CERT_KEY_CONTEXT structure is updated if it exists. If it does not exist, it is created with dwKeySpec and initialized by CERT_KEY_PROV_INFO_PROP_ID. If CERT_STORE_NO_CRYPT_RELEASE_FLAG is not set, the hCryptProv value is implicitly released either when the property is set to NULL or on the final freeing of the CERT_CONTEXT structure.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Revised comment.

return NULL;
if (!ret) {
if (cert) {
CertFreeCertificateContext (cert);
Copy link

Choose a reason for hiding this comment

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

I'm noticing that the type of object returned by CryptQueryObject() above can vary due to the use of CERT_QUERY_CONTENT_FLAG_ALL. I would comment on that directly, but GitHub doesn't seem to let me comment on lines that weren't in the diff. Anyway, I think we probably want CERT_QUERY_CONTENT_CERT above to constrain the return type.

(There's also CERT_QUERY_CONTENT_SERIALIZED_CERT, which is not described clearly in this API reference but I'm finding a 3rd party blog that claims "serialized certificates" are a Microsoft-specific format associated with the .sst file extension, https://medium.com/tenable-techblog/code-for-reading-windows-serialized-certificates-8634d3487ec7)

Doc reference: https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptqueryobject

Copy link

Choose a reason for hiding this comment

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

Noticed this when looking at CertFreeCertificateContext call sites in this file: mongoc_secure_channel_setup_crl is using the wrong free function, there's a separate CertFreeCRLContext API for CRLs.

(Ref: https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptqueryobject)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think we probably want CERT_QUERY_CONTENT_CERT above to constrain the return type.

Replaced call of CryptQueryObject with a call to CertCreateCertificateContext to address.

mongoc_secure_channel_setup_crl is using the wrong free function

Great spot. Debugging shows CryptQueryObject sets the output type (pdwContentType) to CERT_QUERY_CONTENT_CRL. According to documentation:

CERT_QUERY_CONTENT_CRL This parameter receives a pointer to a CRL_CONTEXT structure

Yet the C driver stored the result in a PCCERT_CONTEXT (Pointer-to-Const CERT_CONTEXT) and later passed to CertAddCertificateContextToStore. Despite this, a test with the CRL list appeared to work. I expect this worked "by accident" due to the CERT_CONTEXT and CRL_CONTEXT having the same data layout.

To avoid the incorrect pointer use, latest changes replace the call to CryptQueryObject. A test setting the CRL option is added.

Comment on lines 327 to 328
DWORD encrypted_cert_len = 0;
LPBYTE encrypted_cert = NULL;
Copy link

Choose a reason for hiding this comment

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

"encrypted" is wrong here too, I think they meant "encoded".

if (0 != fseek (file, 0, SEEK_SET)) {
MONGOC_ERROR ("Failed to seek in file: '%s' with error: '%s'",
filename,
bson_strerror_r (errno, errmsg_buf, sizeof errmsg_buf));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
bson_strerror_r (errno, errmsg_buf, sizeof errmsg_buf));
bson_strerror_r (ferror (file), errmsg_buf, sizeof errmsg_buf));

Unlike fopen or ftell, fseek sets ferror in failure, not errno.

kevinAlbs added 12 commits May 15, 2025 12:57
Encrypted keys are not supported.

"encoded" is consistent with naming in WinCrypt API.
Avoids NULL deref if PEM file does not have public cert

Add a regression test.
`CryptQueryObject` is deprecated. The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
Value is not currently checked by caller.
Return true on success to match existing patterns.
Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).

Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
To match error expectations in C99.
Copy link
Collaborator Author

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

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

Latest changes tested with this patch build and include:

  • Fix a NULL-deref if the client PEM file does not include a public certificate.
  • Refactor base64 decoding to a separate helper function.
  • Replace calls to CryptQueryObject.
    • Fixes too-broad search for public key (CERT_QUERY_CONTENT_FLAG_ALL instead of CERT_QUERY_CONTENT_FLAG_CERT).
    • Fixes incorrect pointer loading CRL (was using CERT_CONTEXT instead of CRL_CONTEXT).
  • Test CRL loading.

Comment on lines 81 to 90
MONGOC_ERROR ("Failed to get length of file: '%s'. File too large", filename);
goto fail;
}

if (0 != fseek (file, 0, SEEK_SET)) {
MONGOC_ERROR ("Failed to seek in file: '%s' with error: '%s'",
filename,
bson_strerror_r (errno, errmsg_buf, sizeof errmsg_buf));
goto fail;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not worth having a unique error string for

Agreed. Removed error log for the unlikely-to-encounter errors.

possible overflow when the strlen of an individual key in the file is cast to DWORD

Good spot. This is addressed by replacing the call of CryptQueryObject with CertCreateCertificateContext. Calculating the length is no longer needed.


/* https://msdn.microsoft.com/en-us/library/windows/desktop/aa376573%28v=vs.85%29.aspx
*/
// CertSetCertificateContextProperty takes ownership of `provider`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Revised comment.

@kevinAlbs kevinAlbs requested review from eramongodb and a user May 15, 2025 17:41
@kevinAlbs kevinAlbs requested a review from eramongodb May 15, 2025 20:34
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

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

Looks good! The CRL test and API usage simplifications are a nice addition.

@kevinAlbs kevinAlbs merged commit 378c177 into mongodb:master May 16, 2025
39 of 42 checks passed
kevinAlbs added a commit to kevinAlbs/mongo-c-driver that referenced this pull request May 16, 2025
* Store and free client cert context
* Free on successful load of client cert
** Do not return before `fail` label.
* Free `hKey`
* Free pem file and cert when loading CA file
* Release provider context on error
* NUL terminate pem file contents
** To ensure `strstr` does not read past memory on failure to find.
* Remove unused printf
* Add `read_file_and_null_terminate` helper
* Rename `encrypted_*` to `encoded_*`
** Encrypted keys are not supported with SChannel.
** "encoded" is consistent with naming in WinCrypt API.
* check if `pem_public` is NULL
** Avoids NULL deref if PEM file does not have public cert
* Remove call to `CryptQueryObject` for public cert
** The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
* Remove call to `CryptQueryObject` for CRL
** Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).
** Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
* Remove unused params

---------

Co-authored-by: Ezra Chung <[email protected]>
kevinAlbs added a commit to kevinAlbs/mongo-c-driver that referenced this pull request May 16, 2025
* Store and free client cert context
* Free on successful load of client cert
** Do not return before `fail` label.
* Free `hKey`
* Free pem file and cert when loading CA file
* Release provider context on error
* NUL terminate pem file contents
** To ensure `strstr` does not read past memory on failure to find.
* Remove unused printf
* Add `read_file_and_null_terminate` helper
* Rename `encrypted_*` to `encoded_*`
** Encrypted keys are not supported with SChannel.
** "encoded" is consistent with naming in WinCrypt API.
* check if `pem_public` is NULL
** Avoids NULL deref if PEM file does not have public cert
* Remove call to `CryptQueryObject` for public cert
** The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
* Remove call to `CryptQueryObject` for CRL
** Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).
** Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
* Remove unused params

---------

Co-authored-by: Ezra Chung <[email protected]>
kevinAlbs added a commit to kevinAlbs/mongo-c-driver that referenced this pull request May 16, 2025
* Store and free client cert context
* Free on successful load of client cert
** Do not return before `fail` label.
* Free `hKey`
* Free pem file and cert when loading CA file
* Release provider context on error
* NUL terminate pem file contents
** To ensure `strstr` does not read past memory on failure to find.
* Remove unused printf
* Add `read_file_and_null_terminate` helper
* Rename `encrypted_*` to `encoded_*`
** Encrypted keys are not supported with SChannel.
** "encoded" is consistent with naming in WinCrypt API.
* check if `pem_public` is NULL
** Avoids NULL deref if PEM file does not have public cert
* Remove call to `CryptQueryObject` for public cert
** The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
* Remove call to `CryptQueryObject` for CRL
** Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).
** Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
* Remove unused params

---------

Co-authored-by: Ezra Chung <[email protected]>
kevinAlbs added a commit to kevinAlbs/mongo-c-driver that referenced this pull request May 16, 2025
* Store and free client cert context
* Free on successful load of client cert
** Do not return before `fail` label.
* Free `hKey`
* Free pem file and cert when loading CA file
* Release provider context on error
* NUL terminate pem file contents
** To ensure `strstr` does not read past memory on failure to find.
* Remove unused printf
* Add `read_file_and_null_terminate` helper
* Rename `encrypted_*` to `encoded_*`
** Encrypted keys are not supported with SChannel.
** "encoded" is consistent with naming in WinCrypt API.
* check if `pem_public` is NULL
** Avoids NULL deref if PEM file does not have public cert
* Remove call to `CryptQueryObject` for public cert
** The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
* Remove call to `CryptQueryObject` for CRL
** Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).
** Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
* Remove unused params

---------

Co-authored-by: Ezra Chung <[email protected]>
kevinAlbs added a commit that referenced this pull request May 19, 2025
* Store and free client cert context
* Free on successful load of client cert
** Do not return before `fail` label.
* Free `hKey`
* Free pem file and cert when loading CA file
* Release provider context on error
* NUL terminate pem file contents
** To ensure `strstr` does not read past memory on failure to find.
* Remove unused printf
* Add `read_file_and_null_terminate` helper
* Rename `encrypted_*` to `encoded_*`
** Encrypted keys are not supported with SChannel.
** "encoded" is consistent with naming in WinCrypt API.
* check if `pem_public` is NULL
** Avoids NULL deref if PEM file does not have public cert
* Remove call to `CryptQueryObject` for public cert
** The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
* Remove call to `CryptQueryObject` for CRL
** Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).
** Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
* Remove unused params

---------

Co-authored-by: Ezra Chung <[email protected]>
kevinAlbs added a commit that referenced this pull request May 19, 2025
#2015)

* CDRIVER-5743: NUL terminate string, handle more error cases (#1903)

* NUL termination and error handling fixes for mongoc_secure_channel_setup_ca

* CDRIVER-5743 Followup fix for signedness warning (#1907)

* CDRIVER-3228 fix memory leaks in SChannel cert loading (#2009)

* Store and free client cert context
* Free on successful load of client cert
** Do not return before `fail` label.
* Free `hKey`
* Free pem file and cert when loading CA file
* Release provider context on error
* NUL terminate pem file contents
** To ensure `strstr` does not read past memory on failure to find.
* Remove unused printf
* Add `read_file_and_null_terminate` helper
* Rename `encrypted_*` to `encoded_*`
** Encrypted keys are not supported with SChannel.
** "encoded" is consistent with naming in WinCrypt API.
* check if `pem_public` is NULL
** Avoids NULL deref if PEM file does not have public cert
* Remove call to `CryptQueryObject` for public cert
** The flag `CERT_QUERY_CONTENT_FLAG_ALL`  is likely incorrect (only certificate is expected)
* Remove call to `CryptQueryObject` for CRL
** Return was wrongly stored in a `CERT_CONTEXT` (needed `CRL_CONTEXT`).
** Use `CertCreateCRLContext` for consistency with other PEM-reading functions.
* Remove unused params

---------

Co-authored-by: mdbmes <[email protected]>
Co-authored-by: Ezra Chung <[email protected]>
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