Skip to content

Conversation

htuch
Copy link
Member

@htuch htuch commented Aug 13, 2018

Previously, since we had an implicitly deleted copy constructor, we picked up the implicitly created move constructor. This then performed a move operation on HeaderList, which is not safe to move because it contains iterator elements, which can point to end() and so are not safe when when moved, see the Notes in https://en.cppreference.com/w/cpp/container/list/list.

In this PR, we explicitly mark HeaderList as non-copyable (also suppresses the implicit move constructor), then provide an explicit copy constructor and assignment operator for TestHeaderImpl.

This appears to resolve the leak in #4118.

Risk level: Low
Testing: Unit test added.

Signed-off-by: Harvey Tuch htuch@google.com

Previously, we were picking up the default copy constructor, despite having effectively a copy
constructor for the base class HeaderMap.

This appears to resolve the leak in envoyproxy#4118.

Risk level: Low
Testing: Unit test added.

Signed-off-by: Harvey Tuch <htuch@google.com>
@@ -292,6 +292,9 @@ HeaderMapImpl::HeaderMapImpl(const HeaderMap& rhs) : HeaderMapImpl() {
this);
}

HeaderMapImpl::HeaderMapImpl(const HeaderMapImpl& rhs)
: HeaderMapImpl(static_cast<const HeaderMap&>(rhs)) {}
Copy link
Member

Choose a reason for hiding this comment

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

Sorry I'm confused. How is this different from what the compiler did before?

Copy link
Member Author

Choose a reason for hiding this comment

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

It was synthesizing the default copy constructor, since we only supplied a constructor for the class we were deriving from AFAICT.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's something more going on here though, witness the build failures. Looking into this now.

…estHeaderMapImpl.

Signed-off-by: Harvey Tuch <htuch@google.com>
@htuch
Copy link
Member Author

htuch commented Aug 13, 2018

I think this is the proper fix. By marking HeaderList explicitly non-copyable, this forced the implicit copy assignment to stop happening. I then added an explicit copy assignment operator for TestHeaderMapImpl.

Still puzzled why the copy assignment ever worked (CC @jsedgwick).

},
this);
}
HeaderMapImpl::HeaderMapImpl(const HeaderMapImpl& rhs)
Copy link
Member

Choose a reason for hiding this comment

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

Is this change still needed? It's unclear to me why the compiler won't just pick the other one.

Copy link
Member Author

@htuch htuch Aug 13, 2018

Choose a reason for hiding this comment

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

I think so; the implicit copy constructor is deleted, the above constructor for HeaderMap isn't actually a copy constructor.

Copy link
Member

Choose a reason for hiding this comment

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

I see. OK.

Signed-off-by: Harvey Tuch <htuch@google.com>
htuch added 3 commits August 13, 2018 17:26
Signed-off-by: Harvey Tuch <htuch@google.com>
Signed-off-by: Harvey Tuch <htuch@google.com>
Signed-off-by: Harvey Tuch <htuch@google.com>
mattklein123
mattklein123 previously approved these changes Aug 13, 2018
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks for digging into this!

@htuch
Copy link
Member Author

htuch commented Aug 13, 2018

OK, just chatted with @jsedgwick and have some ideas for how to try and either rationalize this or simplify, will update later tonight.

Signed-off-by: Harvey Tuch <htuch@google.com>
@htuch
Copy link
Member Author

htuch commented Aug 14, 2018

@mattklein123 I just figured this out, I think, with some great help from @jsedgwick. He pointed out that what was going on is not some magic implicit copy constructor generation, which should have been impossible because of the non-copyable HeaderEntryImpl, but instead the compiler was falling back on the implicit move constructor that was generated in lieu.

Moving HeaderList is unsafe; it contains iterator fields that can point to end() and so are not safe when when moved, see the Notes in https://en.cppreference.com/w/cpp/container/list/list. I think this is why I was seeing a leak in #4118.

The solution is basically what I had this arvo; by making HeaderList non-copyable explicitly, this actually disable the implicit move constructor. This then leaves us a clear compiler error message telling us to write a copy constructor/assignment operator in various places, which is what we provide in this PR.

Ultimately, maybe we would want to make HeaderList movable safely, I've left a TODO there. Given we largely are working with HeaderMapPtr when doing std::move(headers), I think we are safe to leave it non-movable, but thought I'd check with you.

Let nobody ever say fuzzing doesn't churn up fun bugs ;)

Signed-off-by: Harvey Tuch <htuch@google.com>
mattklein123
mattklein123 previously approved these changes Aug 14, 2018
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

LGTM, one optional comment.

*/
class HeaderList {
class HeaderList : NonCopyable {
Copy link
Member

Choose a reason for hiding this comment

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

I think it's fine to block move, but why not just block the implicit move constructor inside NonCopyable? I realize it's called NonCopyable but I'm pretty sure in all cases where we use that we also don't want move?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think this change is needed anymore, but fine to keep.

Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

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

I'm really glad you are doing this. Not having copyable HeaderMap was a bit frustrating.

return *this;
}

removePrefix(LowerCaseString(""));
Copy link
Contributor

Choose a reason for hiding this comment

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

WDYT of factoring out a separate clear() method in the public API while you are in here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I would rather not make any API public on HeaderMapImpl that we don't need to. This is a performance sensitive object and we should avoid having folks do slow/inefficient things with it that they don't need to.

Copy link
Contributor

Choose a reason for hiding this comment

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

If someone does need to clear(), I think it'd be better for it to be abstracted at the class interface.
Right now removePrefix(LowerCaseString("")) seems like it might be specific to the implementation?

But a compromise might be to define clear() but make it private or protected, with a note about exposing it in the future if there's a real need.

Copy link
Member Author

Choose a reason for hiding this comment

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

Arguably, nobody should be doing this, removePrefix is a horrible way to clear, it's only suitable for test purposes. I'm going to follow Matt's suggestion and move this to TestHeaderMapImpl only.

void HeaderMapImpl::copyFrom(const HeaderMap& header_map) {
header_map.iterate(
[](const HeaderEntry& header, void* context) -> HeaderMap::Iterate {
// TODO(mattklein123) PERF: Avoid copying here is not necessary.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/is/if/ ?

Do we keep a bit indicating whether a header-map contains owned strings or refs?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will fix typo, performance optimization is orthogonal.

@@ -304,6 +293,22 @@ HeaderMapImpl::HeaderMapImpl(
}
}

void HeaderMapImpl::copyFrom(const HeaderMap& header_map) {
header_map.iterate(
Copy link
Contributor

Choose a reason for hiding this comment

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

If you are defining a copy-ctor I think you should also define an assignment operator (and test it).

https://en.cppreference.com/w/cpp/language/rule_of_three

I see you are defining one in a test subclass, but it doesn't look like you've got one in HeaderMapImpl.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's fair. I will mvoe copy assignment to HeaderMapImpl.

HeaderString key_string;
key_string.setCopy(header.key().c_str(), header.key().size());
HeaderString value_string;
value_string.setCopy(header.value().c_str(), header.value().size());
Copy link
Contributor

Choose a reason for hiding this comment

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

Best practice is to use .data() rather than .c_str() here (x2), although I admit in practice I don't see how they can differ :)

Copy link
Member Author

Choose a reason for hiding this comment

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

There is no .data() on header.value().

@mattklein123 mattklein123 self-assigned this Aug 14, 2018
Signed-off-by: Harvey Tuch <htuch@google.com>
@htuch
Copy link
Member Author

htuch commented Aug 14, 2018

@mattklein123 @jmarantz review feedback in last commit. I don't think we should mark HeaderMapImpl as NonCopyable, since we now do have copy assignment/construction, and these force the implicit move constructor/assignment operators to disappear, which makes things safe.

// implicit move constructor, moving HeaderMapImpl is unsafe (see HeaderList comments).
HeaderMapImpl(const HeaderMapImpl& rhs);
// Safe copy assignment; this is highly inefficient, don't use this except for tests.
HeaderMapImpl& operator=(const HeaderMapImpl& rhs);
Copy link
Member

Choose a reason for hiding this comment

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

Sorry to have more comments on this, but my concern about these changes is that we can accidentally start copying without knowing about it. Is it not possible to make HeaderMapImpl NonCopyable but then add the copy/assignment constructors in TestHeaderMapImpl?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a fair point. Having copy-ctor and assign-operator illegal is fine, but it would be nice to have copyFrom() as an explicit call.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we can do this then. I'm honestly surprised what a mess this all is. If anyone wants ammunition in the language wars that points to C++ being a time suck and unreasonably complicated for even experienced programmers to work with, this is a prime example.

Signed-off-by: Harvey Tuch <htuch@google.com>
jmarantz
jmarantz previously approved these changes Aug 14, 2018
@@ -80,6 +81,9 @@ class HeaderMapImpl : public HeaderMap {
size_t size() const override { return headers_.size(); }

protected:
void copyFrom(const HeaderMap& rhs);
Copy link
Contributor

Choose a reason for hiding this comment

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

comment that these are protected because they are suspected to be slow, and should only be used in tests?

mattklein123
mattklein123 previously approved these changes Aug 14, 2018
*/
class HeaderList {
class HeaderList : NonCopyable {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this change is needed anymore, but fine to keep.

Signed-off-by: Harvey Tuch <htuch@google.com>
@htuch htuch dismissed stale reviews from mattklein123 and jmarantz via aa008ff August 14, 2018 17:46
@htuch
Copy link
Member Author

htuch commented Aug 14, 2018

I rather mark things everywhere as NonCopyable to make it as explicit as possible why/where we get into this situation. Long term, we should rethink the use of iterators to end(), but let's just land this fix for now.

@htuch htuch merged commit b150d61 into envoyproxy:master Aug 14, 2018
@htuch htuch deleted the header-map-copy branch August 14, 2018 19:56
@zuercher zuercher mentioned this pull request Aug 14, 2018
snowp pushed a commit to snowp/envoy that referenced this pull request Aug 14, 2018
* origin/master:
  fix flaky RBAC integration test. (envoyproxy#4147)
  header_map: copy constructor for HeaderMapImpl. (envoyproxy#4129)
  test: moving websocket tests to using HTTP codec. (envoyproxy#4143)
  upstream: init host hc value based on hc value from other priorities (envoyproxy#3959)

Signed-off-by: Snow Pettersen <snowp@squareup.com>
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.

3 participants