Skip to content
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

Closed
wants to merge 4 commits into from

Conversation

tniessen
Copy link
Member

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), or vcbuild test (Windows) passes
  • commit message follows commit guidelines

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. labels Oct 11, 2018

Error err = sign->SignFinal(
buf,
buf_len,
len >= 2 && !args[1]->IsNull() ? *passphrase : nullptr,
md_value,
&md_value,
Copy link
Contributor

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.

@@ -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;
Copy link
Contributor

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.

&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)
Copy link
Contributor

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.

return 0;
return false;

*sig_len = static_cast<size_t>(EVP_PKEY_size(pkey.get()));
Copy link
Member

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. :-)

return false;

*sig_len = static_cast<size_t>(EVP_PKEY_size(pkey.get()));
*md = reinterpret_cast<unsigned char*>(node::Malloc(*sig_len));
Copy link
Member

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.
@tniessen

This comment has been minimized.

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)
Copy link
Member

Choose a reason for hiding this comment

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

sig.release() :)

Copy link
Member Author

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!!!

Copy link
Contributor

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).

@addaleax
Copy link
Member

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 :)

@Guillerbr
Copy link

good!

@tniessen
Copy link
Member Author

Btw, I’m not sure about the commit message, because it seems unlikely that this actually reduces memory usage.

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.

@addaleax
Copy link
Member

@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.

@tniessen
Copy link
Member Author

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 SignFinal, not of the whole process, and I think that's correct.

Copy link
Contributor

@refack refack left a comment

Choose a reason for hiding this comment

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

Some changes

}

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,
Copy link
Contributor

@refack refack Oct 14, 2018

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.

*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);
Copy link
Contributor

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?

@@ -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;
Copy link
Contributor

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.

Copy link
Member

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

Copy link
Contributor

@refack refack Oct 14, 2018

Choose a reason for hiding this comment

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

  1. If we can do this on the stack it's free.
  2. If we use a vector with move semantics, there's no allocation until we resize.
  3. As you mentioned, this change's main benefit is readability.

And we have several guidelines that this breaks:
Google's:

  1. Local Variables - Place a function's variables in the narrowest scope possible, and initialize variables in the declaration.
  2. Output Parameters - Prefer using return values rather than output parameters. If output-only parameters are used they should appear after input parameters.
  3. C++11 Use libraries and language extensions from C++11 when appropriate.

C++CG:

  1. ES.20: Always initialize an object
  2. F.20: For “out” output values, prefer return values to output parameters
  3. ES.1: Prefer the standard library to other libraries and to “handcrafted code”

Copy link
Member

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)

Copy link
Contributor

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:

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

return sign->CheckThrow(std::get<Error>(ret));

MallocedBuffer<unsigned char> sig =
std::get<MallocedBuffer<unsigned char>>(std::move(ret));
Copy link
Contributor

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;
Copy link
Contributor

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;

@refack
Copy link
Contributor

refack commented Oct 17, 2018

@tniessen thanks for following up. IMHO this looks much better (no uninitialized variables, and no need for out params.
I left a few suggestions, but they are pseudocode...

@@ -446,7 +446,7 @@ struct MallocedBuffer {

inline bool is_empty() const { return data == nullptr; }

MallocedBuffer() : data(nullptr) {}
MallocedBuffer() : data(nullptr), size(0) {}
Copy link
Contributor

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.

@tniessen
Copy link
Member Author

tniessen commented Oct 17, 2018

@tniessen
Copy link
Member Author

Landed in 7bd2912, thank you for reviewing.

@tniessen tniessen closed this Oct 19, 2018
tniessen added a commit that referenced this pull request Oct 19, 2018
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]>
addaleax added a commit to addaleax/node that referenced this pull request Oct 20, 2018
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
addaleax added a commit that referenced this pull request Oct 21, 2018
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]>
jasnell pushed a commit that referenced this pull request Oct 21, 2018
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]>
jasnell pushed a commit that referenced this pull request Oct 21, 2018
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]>
@MylesBorins
Copy link
Contributor

Should this land on v10.x? It lands cleanly but seems like perhaps it should be in a release a bit longer first. Thoughts?

codebytere pushed a commit that referenced this pull request Dec 13, 2018
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]>
MylesBorins pushed a commit that referenced this pull request Dec 26, 2018
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]>
@codebytere codebytere mentioned this pull request Jan 4, 2019
BethGriggs pushed a commit that referenced this pull request Oct 18, 2019
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants