crypto: reduce memory usage of SignFinal#23427
crypto: reduce memory usage of SignFinal#23427tniessen wants to merge 4 commits intonodejs:masterfrom
Conversation
src/node_crypto.cc
Outdated
There was a problem hiding this comment.
is this function, SignFinal overloaded? because an array value, and a pointer to an uninitialized pointer are very different.
src/node_crypto.cc
Outdated
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
|
|
||
| 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.
Ohhhhh right thank you so much!!!
There was a problem hiding this comment.
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 |
src/node_crypto.cc
Outdated
| const char* passphrase, | ||
| unsigned char* sig, | ||
| unsigned int* sig_len, | ||
| MallocedBuffer<unsigned char>* buffer, |
There was a problem hiding this comment.
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.
why are you constructing a new MallocedBuffer?
src/node_crypto.cc
Outdated
| 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.
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.
@refack That would undo the purpose of this change, which is to avoid extra allocations
There was a problem hiding this comment.
- 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.
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.
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
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.
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.
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. |
| inline bool is_empty() const { return data == nullptr; } | ||
|
|
||
| MallocedBuffer() : data(nullptr) {} | ||
| MallocedBuffer() : data(nullptr), size(0) {} |
There was a problem hiding this comment.
👍 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 <anna@addaleax.net> Reviewed-By: Refael Ackermann <refack@gmail.com>
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 <joyeec9h3@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
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 <anna@addaleax.net> Reviewed-By: Refael Ackermann <refack@gmail.com>
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 <joyeec9h3@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
|
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 <anna@addaleax.net> Reviewed-By: Refael Ackermann <refack@gmail.com>
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 <anna@addaleax.net> Reviewed-By: Refael Ackermann <refack@gmail.com>
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 <joyeec9h3@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Refael Ackermann <refack@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
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