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

Protect ACLFilledChecklist heap allocations from leaking #1870

Conversation

eduard-bagdasaryan
Copy link
Contributor

Non-blocking ACL checks follow this code pattern:

const auto ch = new ACLFilledChecklist(...);
fillWithInformation(ch); // may throw
ch->nonBlockingCheck(&throwingCallback); // may delete ch or throw!
// ch may be a dangling raw pointer here

The checklist object is leaked if an exception is thrown by
fillWithInformation() (and the code it calls) or by nonBlockingCheck()
(and the code it calls, including, in some cases, throwingCallback()).
Use std::unique_ptr to avoid such leaks.

eduard-bagdasaryan and others added 13 commits July 4, 2024 17:54
The common approach when dealing with ACLFilledChecklist non-blocking
checks consists of three steps:

1. create an ACLFilledChecklist object on heap
2. configure the object
3. invoke ACLFilledChecklist::nonBlockingCheck()

Memory leak occurs if an exception is thrown between (1) and (3). To avoid
leaks, whe should rely on RAII mechanism instead of raw pointers.
After introducing ACLFilledChecklist::Make() API the calling code got
unique_ptr<> instead of ACLFilledChecklist*, and its semantics remained
the same. However, since  nonBlockingCheck() still expects a raw
pointer (which it takes ownership of), the unique_ptr<> should be
released before the call. The new nonBlockingCheck() now encapsulates
this requirement.
There is no difference for the CBDATA_CLASS_WITH_MAKE()-calling class.
However, if some future code (incorrectly) creates a child of that
class, then we want build to fail. Creating a child requires using
something like CBDATA_INTERMEDIATE()/CBDATA_CHILD() for reasons
documented in CBDATA_INTERMEDIATE() description. We cannot _guarantee_
that such a build would fail, but making new() private will help in
cases where child code calls new().

Besides, tighter restrictions should raise fewer questions, and it is
always easier to relax a restriction than to add one.
This change arguably makes this macro-based code slightly easier to grok
and clarifies CBDATA_CLASS_WITH_MAKE() intent.
The pointer returned by ACLFilledChecklist::Make() cannot be used for
those purposes (because there can be only one checklist/cbdata owner)
and, to prevent memory leaks, a different kind of pointer should be used
during object configuration.
Also updated stale references to aclCheckFast() and aclNBCheck().
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

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

Thank you.

@@ -2501,7 +2501,7 @@ ConnStateData::postHttpsAccept()
ClientHttpRequest *http = context ? context->http : nullptr;
const char *log_uri = http ? http->log_uri : nullptr;
acl_checklist->syncAle(request, log_uri);
acl_checklist->nonBlockingCheck(httpsSslBumpAccessCheckDone, this);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), httpsSslBumpAccessCheckDone, this);
Copy link
Contributor

Choose a reason for hiding this comment

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

This explicit std::move() call (and similar calls added in this PR) is one of those exceptional cases where forcing developer to write (and code reader to see) this explicit std::move call is desirable: We want to emphasize that acl_checklist may be gone after this NonBlockingCheck() call returns.

BTW, GCC and clang do not do that for us automatically, but we plan to enhance our CI tests to automatically check Squid code for use-after-move errors.

This comment does not request any PR changes.

acl_checklist->banAction(Acl::Answer(ACCESS_ALLOWED, Ssl::bumpNone));
acl_checklist->banAction(Acl::Answer(ACCESS_ALLOWED, Ssl::bumpClientFirst));
acl_checklist->banAction(Acl::Answer(ACCESS_ALLOWED, Ssl::bumpServerFirst));
fillChecklist(*acl_checklist);
acl_checklist->nonBlockingCheck(httpsSslBumpStep2AccessCheckDone, this);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), httpsSslBumpStep2AccessCheckDone, this);
Copy link
Contributor

Choose a reason for hiding this comment

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

If others prefer, we can simplify this to remove ACLFilledChecklist prefix by making NonBlockingCheck() into a global function:

Suggested change
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), httpsSslBumpStep2AccessCheckDone, this);
NonBlockingCheck(std::move(acl_checklist), httpsSslBumpStep2AccessCheckDone, this);

or

Suggested change
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), httpsSslBumpStep2AccessCheckDone, this);
Acl::NonBlockingCheck(std::move(acl_checklist), httpsSslBumpStep2AccessCheckDone, this);

Due to fairly unique parameter types, such a global function is unlikely to clash with other non-blocking check functions. Please add a comment if you want to see this kind of change (at the cost of adding a corresponding friend declaration to checklist class type).

This comment, byt itself, does not request any PR changes.

/// Unlike regular Foo::Pointer types, this smart pointer is meant for use
/// during checklist configuration only, when it provides exception safety.
/// Any other/long-term checklist storage requires CbcPointer or equivalent.
using MakingPointer = std::unique_ptr<ACLFilledChecklist>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Another possible type name that still addresses the documented "this is not our regular Pointer" concern is UniquePointer:

Suggested change
using MakingPointer = std::unique_ptr<ACLFilledChecklist>;
using UniquePointer = std::unique_ptr<ACLFilledChecklist>;

I prefer MakingPointer because it is more specific to this smart pointer primary purpose, but I acknowledge that "making pointer" is ambiguous.

This comment does not request any PR changes.

@rousskov rousskov added the M-cleared-for-merge https://github.com/measurement-factory/anubis#pull-request-labels label Aug 9, 2024
squid-anubis pushed a commit that referenced this pull request Aug 9, 2024
Non-blocking ACL checks follow this code pattern:

    const auto ch = new ACLFilledChecklist(...);
    fillWithInformation(ch); // may throw
    ch->nonBlockingCheck(&throwingCallback); // may delete ch or throw!
    // ch may be a dangling raw pointer here

The checklist object is leaked if an exception is thrown by
fillWithInformation() (and the code it calls) or by nonBlockingCheck()
(and the code it calls, including, in some cases, throwingCallback()).
Use std::unique_ptr to avoid such leaks.
@squid-anubis squid-anubis added the M-waiting-staging-checks https://github.com/measurement-factory/anubis#pull-request-labels label Aug 9, 2024
@squid-anubis squid-anubis added M-merged https://github.com/measurement-factory/anubis#pull-request-labels and removed M-waiting-staging-checks https://github.com/measurement-factory/anubis#pull-request-labels M-cleared-for-merge https://github.com/measurement-factory/anubis#pull-request-labels labels Aug 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
M-merged https://github.com/measurement-factory/anubis#pull-request-labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants