-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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
crypto: reduce memory usage of SignFinal #23427
Conversation
src/node_crypto.cc
Outdated
|
||
Error err = sign->SignFinal( | ||
buf, | ||
buf_len, | ||
len >= 2 && !args[1]->IsNull() ? *passphrase : nullptr, | ||
md_value, | ||
&md_value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this function, SignFinal
overloaded? because an array value, and a pointer to an uninitialized pointer are very different.
src/node_crypto.cc
Outdated
@@ -3597,22 +3594,22 @@ void Sign::SignFinal(const FunctionCallbackInfo<Value>& args) { | |||
int salt_len = args[3].As<Int32>()->Value(); | |||
|
|||
ClearErrorOnReturn clear_error_on_return; | |||
unsigned char md_value[8192]; | |||
unsigned int md_len = sizeof(md_value); | |||
unsigned char* md_value; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest initializing to nullptr, even though its going to be allocated by SignFinal, unless leaving floating pointers around is the convention in node_crypto.cc.
src/node_crypto.cc
Outdated
&md_len, | ||
padding, | ||
salt_len); | ||
if (err != kSignOk) | ||
return sign->CheckThrow(err); | ||
|
||
Local<Object> rc = | ||
Buffer::Copy(env, reinterpret_cast<char*>(md_value), md_len) | ||
Buffer::New(env, reinterpret_cast<char*>(md_value), md_len) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry Tobias, I'm just looking at the PR, not digging into the APIs. Is md_value a pointer to memory owned elsewhere? Who is responsible for deallocating it? I can't tell from these APIs calls if its leaking, if it is a pointer to memory that is managed as part of sign
so doesn't need deallocating, or whether its allocated and Buffer:New() is accepting responsibility for deallocating it.
src/node_crypto.cc
Outdated
return 0; | ||
return false; | ||
|
||
*sig_len = static_cast<size_t>(EVP_PKEY_size(pkey.get())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EVP_PKEY_size()
returns an int. Casting to unsigned means a negative value will wrap around.
While unlikely, there are one or two key types where this function can potentially fail (with RSA and DSA keys.)
Long story short, please CHECK_GE(key_size, 0)
first. :-)
src/node_crypto.cc
Outdated
return false; | ||
|
||
*sig_len = static_cast<size_t>(EVP_PKEY_size(pkey.get())); | ||
*md = reinterpret_cast<unsigned char*>(node::Malloc(*sig_len)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allocation appears to leak when returning false below.
The fixed-size buffer on the stack is unnecessary and way too large for most applications. This change removes it and allocates the required memory directly instead of copying into heap later.
bff5fd8
to
4b4b71b
Compare
This comment has been minimized.
This comment has been minimized.
src/node_crypto.cc
Outdated
padding, | ||
salt_len); | ||
if (err != kSignOk) | ||
return sign->CheckThrow(err); | ||
|
||
Local<Object> rc = | ||
Buffer::Copy(env, reinterpret_cast<char*>(md_value), md_len) | ||
Buffer::New(env, reinterpret_cast<char*>(sig.data), sig.size) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sig.release()
:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohhhhh right thank you so much!!!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very important point... I've been thinking about how to minimize that.
I was thinking about suggesting a unique_ptr<T>
in MallocedBuffer
instead of the naked pointer. That will make users either buf.get()
or buf.release()
(or pass by unique_ptr
value).
CI: https://ci.nodejs.org/job/node-test-pull-request/17853/ Btw, I’m not sure about the commit message, because it seems unlikely that this actually reduces memory usage. This does clean the code up a bit, and it saves a small extra copy operation, though :) |
good! |
In theory, it should (8KB is quite a large buffer compared to usual memory page sizes). Whether that actually happens is up to the OS / compiler. I can change the commit message if you'd like. |
@tniessen Stack pages should only be initialized once, when the stack exceeds a certain threshold, and there should be no other runtime allocation costs or extra memory usage – so, essentially cost-free. |
As far as I know, that's true for most if not all kernels, but ultimately, it is an implementation detail. I agree that this PR is unlikely to change the number of allocated pages in practice, but the commit message only says that this reduces the memory usage of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some changes
src/node_crypto.cc
Outdated
} | ||
|
||
SignBase::Error Sign::SignFinal(const char* key_pem, | ||
int key_pem_len, | ||
const char* passphrase, | ||
unsigned char* sig, | ||
unsigned int* sig_len, | ||
MallocedBuffer<unsigned char>* buffer, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you return an std::pair
instead of the out-param?
Or even leverage move semantic, to pass by value.
src/node_crypto.cc
Outdated
*sig_len = sltmp; | ||
return 1; | ||
EVP_PKEY_sign(pkctx.get(), sig.data, &sig_len, m, m_len) > 0) { | ||
return MallocedBuffer<unsigned char>(sig.release(), sig_len); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are you constructing a new MallocedBuffer
?
src/node_crypto.cc
Outdated
@@ -3618,22 +3618,20 @@ void Sign::SignFinal(const FunctionCallbackInfo<Value>& args) { | |||
int salt_len = args[3].As<Int32>()->Value(); | |||
|
|||
ClearErrorOnReturn clear_error_on_return; | |||
unsigned char md_value[8192]; | |||
unsigned int md_len = sizeof(md_value); | |||
MallocedBuffer<unsigned char> sig; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the MallocedBuffer
abstraction fails to fit, maybe create an std::vector<unsigned char>
here and conditionally fill it in SignFinal
.
If you know the maximal size you could even use an std::array<unsigned char>
and save all the memory allocations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@refack That would undo the purpose of this change, which is to avoid extra allocations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- If we can do this on the stack it's free.
- If we use a vector with move semantics, there's no allocation until we resize.
- As you mentioned, this change's main benefit is readability.
And we have several guidelines that this breaks:
Google's:
- Local Variables - Place a function's variables in the narrowest scope possible, and initialize variables in the declaration.
- Output Parameters - Prefer using return values rather than output parameters. If output-only parameters are used they should appear after input parameters.
- C++11 Use libraries and language extensions from C++11 when appropriate.
C++CG:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Local Variables - Place a function's variables in the narrowest scope possible, and initialize variables in the declaration.
I don’t see that being broken here.
Output Parameters - Prefer using return values rather than output parameters. If output-only parameters are used they should appear after input parameters.
That would go away if we implemented a std::pair
return type, as you suggested. I 👍’ed that comment.
C++11 Use libraries and language extensions from C++11 when appropriate.
I don’t see that being broken here.
ES.20: Always initialize an object
I don’t see that being broken here. Again, the rule explicitly lists it as a valid exception if the object is about to be initialized.
F.20: For “out” output values, prefer return values to output parameters
(same as above, std::pair
would “solve” this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if we use a pair
we get rid of most of the anti-patterns. That's my requested-change.
As for whether MallocedBuffer
is the best abstraction for this case, that's an open question I pose to @tniessen.
IMHO it breaks since we need to resize in:
Line 3542 in e5bd9d0
return MallocedBuffer<unsigned char>(sig.release(), sig_len); |
And a resizable contiguous container is literally the definition of
std::vector
. AFAIU since it's a part of the language compilers can minimize heap allocations
src/node_crypto.cc
Outdated
return sign->CheckThrow(std::get<Error>(ret)); | ||
|
||
MallocedBuffer<unsigned char> sig = | ||
std::get<MallocedBuffer<unsigned char>>(std::move(ret)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to move the buffer not the ret:
MallocedBuffer<unsigned char> sig = std::move(std::get<MallocedBuffer<unsigned char>>(ret));
Or:
MallocedBuffer<unsigned char> sig(std::get<MallocedBuffer<unsigned char>>(ret));
Or my favorite:
const auto& sig = std::get<MallocedBuffer<unsigned char>>(ret);
const char* passphrase, | ||
int padding, | ||
int salt_len) { | ||
MallocedBuffer<unsigned char> buffer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also possible:
auto ret = std::make_pair<SignBase::Error, MallocedBuffer<unsigned char>>(kSignNotInitialised, nullptr);
then later:
std::get<Error>(ret) = kSignNotInitialised;
etc;
@tniessen thanks for following up. IMHO this looks much better (no uninitialized variables, and no need for out params. |
@@ -446,7 +446,7 @@ struct MallocedBuffer { | |||
|
|||
inline bool is_empty() const { return data == nullptr; } | |||
|
|||
MallocedBuffer() : data(nullptr) {} | |||
MallocedBuffer() : data(nullptr), size(0) {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 I had a patch floating around I wanted to submit with this.
Landed in 7bd2912, thank you for reviewing. |
The fixed-size buffer on the stack is unnecessary and way too large for most applications. This change removes it and allocates the required memory directly instead of copying into heap later. PR-URL: #23427 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
Using the non-indexed variant of `std::get<>` broke Travis CI. Also, this allows us to be a bit more concise when returning from `SignFinal()` due to some error condition. Refs: nodejs#23427
Using the non-indexed variant of `std::get<>` broke Travis CI. Also, this allows us to be a bit more concise when returning from `SignFinal()` due to some error condition. Refs: #23427 PR-URL: #23779 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
The fixed-size buffer on the stack is unnecessary and way too large for most applications. This change removes it and allocates the required memory directly instead of copying into heap later. PR-URL: #23427 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
Using the non-indexed variant of `std::get<>` broke Travis CI. Also, this allows us to be a bit more concise when returning from `SignFinal()` due to some error condition. Refs: #23427 PR-URL: #23779 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
Should this land on v10.x? It lands cleanly but seems like perhaps it should be in a release a bit longer first. Thoughts? |
The fixed-size buffer on the stack is unnecessary and way too large for most applications. This change removes it and allocates the required memory directly instead of copying into heap later. PR-URL: #23427 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
The fixed-size buffer on the stack is unnecessary and way too large for most applications. This change removes it and allocates the required memory directly instead of copying into heap later. PR-URL: #23427 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
Using the non-indexed variant of `std::get<>` broke Travis CI. Also, this allows us to be a bit more concise when returning from `SignFinal()` due to some error condition. Refs: #23427 PR-URL: #23779 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
The fixed-size buffer on the stack is unnecessary and way too large for most applications. This change removes it and allocates the required memory directly instead of copying into heap later.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes