-
Notifications
You must be signed in to change notification settings - Fork 2.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
Add a 'Scoped Buffer' implementation as smart-pointer #2987
Conversation
Platform::Memory.
For reference, in a malloc/free wrapper for scoped memory:
results in this on gcc:
|
Did you test single vs multiple returns on godbolt? I think that's more likely where costs will be incurred.
|
src/lib/support/ScopedBuffer.h
Outdated
operator bool() const { return mBuffer != nullptr; } | ||
|
||
/** Take over managing memory from the specified buffer */ | ||
ScopedMemoryBufferBase & Aquire(void * 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.
"Acquire". Maybe "Adopt" is a better name?
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.
how does the instance know where to call to free the pointer? the buffer in question has to be understood by the Impl's static MemoryFree() function?
I'd really like to see a callback (or a closure) passed in here.
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 believe this is a crutch for existing code if memory is already managed 'the old way'.
We can assume that any porting/new code can start to use this code from the start and then drop this method.
* Releases the undelying buffer. Buffer stops being managed and will not be | ||
* auto-freed. | ||
*/ | ||
void * 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.
Do we want to call this "Release", or "Forget"? I guess "Release" more closely matches the unique_ptr
API, but then we should use "Reset" instead of "Acquire", maybe....
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 dropped aquire and thinking to keep release for matching with unique_ptr. This class is basically uniqe_ptr but without exception handling.
@@ -867,7 +868,7 @@ CHIP_ERROR GenericConfigurationManagerImpl<ImplClass>::_ComputeProvisioningHash( | |||
using HashAlgo = chip::Crypto::Hash_SHA256_stream; | |||
|
|||
HashAlgo hash; | |||
uint8_t * dataBuf = NULL; | |||
chip::Platform::ScopedMemoryBuffer dataBuf; |
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 inside namespace chip
, right? Why the prefix?
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.
Amazingly enough compiler complained about Platform being ambigous, so I used it as such.
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.
Specifically at least chip::Platform and chip::System::Platform exist and for code that has 'using' on both chip and system, we get conflicts.
We should not get conflicts if headers had a rule of 'no using inside headers' but the ported code does not enforce that so we are stuck until we clean up code. Would prefer that cleanup to be independent of this PR.
static void MemoryFree(void * p) { chip::Platform::MemoryFree(p); } | ||
static void * MemoryAlloc(size_t size) { return chip::Platform::MemoryAlloc(size); } | ||
static void * MemoryAlloc(size_t size, bool longTerm) { return chip::Platform::MemoryAlloc(size, longTerm); } | ||
static void * MemoryCalloc(size_t num, size_t size) { return chip::Platform::MemoryCalloc(num, 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.
All of this stuff is inside namespace chip
, so why the prefixes?
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.
Seems we have some include ambiguity. I did not try to fix it but rather just be explicit. It looks slightly annoying, but not unreadable.
|
||
#include <nlunit-test.h> | ||
|
||
using namespace chip; |
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 should obviate the need for the chip::
in various places in this file.
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.
Dropped the line. we don't have too many chip:: remaining so this should be fine. Somehow Platform cannot be without chip:: prefix so the worst offender cannot be removed.
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.
Actually rechecked and in this context I could remove chip:: and keep just Platform. Still unsure if worth it.
CHIP_ERROR retval = CHIP_NO_ERROR; | ||
char * encodedData = nullptr; | ||
CHIP_ERROR retval = CHIP_NO_ERROR; | ||
chip::Platform::ScopedMemoryBuffer encodedData; |
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 inside namespace chip
?
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.
You would think so, but Inet and storage have different ones :(
../../src/platform/Linux/CHIPLinuxStorageIni.cpp:222:79: error: reference to ‘Platform’ is ambiguous
222 | CHIP_ERROR ChipLinuxStorageIni::GetBinaryBlobDataAndLengths(const char * key, Platform::ScopedMemoryBuffer & encodedData,
| ^~~~~~~~
In file included from ../../src/lib/core/CHIPCore.h:39,
from ../../src/include/platform/CHIPDeviceLayer.h:25,
from ../../src/include/platform/internal/CHIPDeviceLayerInternal.h:23,
from ../../src/platform/Linux/CHIPLinuxStorageIni.cpp:31:
../../src/inet/InetLayer.h:111:11: note: candidates are: ‘namespace chip::Inet::Platform { }’
111 | namespace Platform {
| ^~~~~~~~
In file included from ../../src/platform/Linux/CHIPLinuxStorageIni.h:30,
from ../../src/platform/Linux/CHIPLinuxStorageIni.cpp:30:
../../src/include/platform/PersistedStorage.h:34:11: note: ‘namespace chip::Platform { }’
34 | namespace Platform {
I don't want to refactor too much, so keeping the super specific bit.
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.
Yeah, I didn't know we had multiple Platform
namespaces. Just ignore all the namespace comments.
void * Ptr() { return mBuffer; } | ||
const void * Ptr() const { return mBuffer; } | ||
|
||
template <typename T> |
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 wonder whether it makes sense to make the following changes:
- Add a template parameter for the type of the things in the buffer (to either the base class or the underlying class). Let's call that type
Type
. static_assert
thatType
is a POD type, to avoid issues with people putting things with destructors (which will not get run) in the buffer- If we have static knowledge about alignment of the things our allocator returns maybe
static_assert
thatType
does not have stronger alignment requirements. - Add conversion operators returning
Type*
andconst Type*
. - Maybe add an
operator[]
returningType&
(and a const equivalent).
This would still allow effectively doing reinterpret_cast
via the Ptr
method if we want to allow that, but would also allow "normal" use to specify the type up front and not have to mention it at every place where the buffer is actually being used. The various places in the changeset that use someBuffer.Ptr<sometype>()
could just use someBuffer
.
Thoughts?
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 considered that however my intent is to avoid template explosion (this was the main objection I heard about using templates in embedded). I do not want duplicate implementations inside flash for uint8_t, char, etc.
In this approach, we have only the Get duplicated, but that one is a trivial function and inlined, so it should go away and we get 0 cost abstraction.
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.
We may refactor it later... maybe subclassing works without exploding things (just make cast operators easy to use). I would defer it for the future though. At first I thought that this Memory alloc/free is used all over in chip, but in reality we have very limited places where we actually use it.
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.
maybe subclassing works without exploding things
Right. Codesize explosion is a good reason to not template the base class. Templating the derived class, and making sure that the templated thing only has inline methods, would avoid the codesize problems, though.
I'm probably ok with this change as a followup, especially since it would involve codesize measurements, but I do think we should make it, not just for ease of use but also because it enables us to add safety asserts (like the is_pod
assertion I suggested). Are you willing to take the time to do 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.
I can do it, please add a PR and assign it to me.
I am unsure timing wise, however it seems small enough of a change that I can do it.
Also thinking we probably want a implementation for 'secure memory' to call a clear on every free. Our existing code seems somewhat buggy in that regard.
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 agree with Boris, the current implementation strikes me as type-unsafe and we should be able to fix it without increasing code size.
I would suggest removing the template parameter from Ptr<> in this initial PR, that's really just hiding a typecast under a less obvious syntax now. Potentially unsafe casts are the last thing we want to hide in a benign-looking accessor function.
Also, this is excellent. :) |
} | ||
|
||
// Read the device intermediate CA certificates. | ||
err = Impl()->_GetManufacturerDeviceIntermediateCACerts(dataBuf, certsLen, certsLen); | ||
err = Impl()->_GetManufacturerDeviceIntermediateCACerts(dataBuf.Ptr<uint8_t>(), certsLen, certsLen); |
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 is this <uint8_t> templating necessary here, but not below for ClearSecretData?
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 tried verbatim copy. Data was uint8_t by default hence conversions everywhere, but clear secret data takes void* I think. I did not check the rest.
Speaking of which: clearsecretdata is called on the last free, however we have an internal alloc that does NOT clear it. Unsure if intentional or not, so I kept 'as is' but I do assume a bug where using this class would have made easier to spot.
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.
clearsecretdata() takes a uint8_t*
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.
Hmmm... new theory (verified though): when I do 'run functional and unit tests' in vscode, this file does not get called at all, so it does not notice that the clearsecretdata call is bad.
I updated it blindly and will rely on CI to check things. This is very odd... is this file even used?
ScopedMemoryBufferBase & Alloc(size_t size) | ||
{ | ||
Free(); | ||
mBuffer = Impl::MemoryAlloc(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.
is there any instance data for the Impl? what if we want to allocate from pools, or allocate and free across linking boundaries?
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.
Impl was just just because I needed to test things. In reality these are supposed to be Platform::Memory* methods which are not inside a class.
Static polyphormisms seems awkward, but at least has no cost and is testable.
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.
Not sure if that's useful at all, but you could probably support stateful allocators, by reversing the class hierarchy and following STL convention:
struct DefaultAllocator
{
static void MemoryFree(void * p) { chip::Platform::MemoryFree(p); }
static void * MemoryAlloc(size_t size) { return chip::Platform::MemoryAlloc(size); }
static void * MemoryAlloc(size_t size, bool longTerm) { return chip::Platform::MemoryAlloc(size, longTerm); }
static void * MemoryCalloc(size_t num, size_t size) { return chip::Platform::MemoryCalloc(num, size); }
};
template <class Allocator=DefaultAllocator>
class ScopedMemoryBuffer : Allocator
{
public:
explicit ScopedMemoryBufferBase(const Allocator& a) : Allocator(a) {}
...
};
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 like the suggestion, however I am unsure of the effect of those on template code explosion. I believe we do not use std pointers because exceptions and because potential code bloat.
I would be happy to review this separately, however as a first pass I would prefer code that I was able to double-check with godbolt.
void * Ptr() { return mBuffer; } | ||
const void * Ptr() const { return mBuffer; } | ||
|
||
template <typename T> |
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 agree with Boris, the current implementation strikes me as type-unsafe and we should be able to fix it without increasing code size.
I would suggest removing the template parameter from Ptr<> in this initial PR, that's really just hiding a typecast under a less obvious syntax now. Potentially unsafe casts are the last thing we want to hide in a benign-looking accessor function.
Oh ... just had a refactor to address Michael/Boris coments regarding typesafety. Will submit them as a separate PR. |
Problem
Pairing malloc and frees is easily error prone. In C++ we can use RAII.
Summary of Changes
Defines a smart pointer around Platform::MemoryAlloc and friends. Tested via Godbolt that this is a 0-cost abstraction.
used this new class in a couple of places. Specifically for linux storage, I also refactored the layout of some functions to avoid the large if nesting (was even missing an out of memory check which would have made it larger) at the expese of not having a single return (I believe we overly try for a single return and this hurts readability).
Fixes #2896