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

Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 23 additions & 22 deletions src/acl/Checklist.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,6 @@ class ACLChecklist
ACLChecklist();
virtual ~ACLChecklist();

/**
* Start a non-blocking (async) check for a list of allow/deny rules.
* Each rule comes with a list of ACLs.
*
* The callback specified will be called with the result of the check.
*
* The first rule where all ACLs match wins. If there is such a rule,
* the result becomes that rule keyword (ACCESS_ALLOWED or ACCESS_DENIED).
*
* If there are rules but all ACL lists mismatch, an implicit rule is used.
* Its result is the negation of the keyword of the last seen rule.
*
* Some ACLs may stop the check prematurely by setting an exceptional
* check result (e.g., ACCESS_AUTH_REQUIRED) instead of declaring a
* match or mismatch.
*
* If there are no rules to check at all, the result becomes ACCESS_DUNNO.
* Calling this method with no rules to check wastes a lot of CPU cycles
* and will result in a DBG_CRITICAL debugging message.
*/
void nonBlockingCheck(ACLCB * callback, void *callback_data);

/**
* Perform a blocking (immediate) check for a list of allow/deny rules.
* Each rule comes with a list of ACLs.
Expand Down Expand Up @@ -157,6 +135,29 @@ class ACLChecklist
/// remember the name of the last ACL being evaluated
void setLastCheckedName(const SBuf &name) { lastCheckedName_ = name; }

protected:
/**
* Start a non-blocking (async) check for a list of allow/deny rules.
* Each rule comes with a list of ACLs.
*
* The callback specified will be called with the result of the check.
*
* The first rule where all ACLs match wins. If there is such a rule,
* the result becomes that rule keyword (ACCESS_ALLOWED or ACCESS_DENIED).
*
* If there are rules but all ACL lists mismatch, an implicit rule is used.
* Its result is the negation of the keyword of the last seen rule.
*
* Some ACLs may stop the check prematurely by setting an exceptional
* check result (e.g., ACCESS_AUTH_REQUIRED) instead of declaring a
* match or mismatch.
*
* If there are no rules to check at all, the result becomes ACCESS_DUNNO.
* Calling this method with no rules to check wastes a lot of CPU cycles
* and will result in a DBG_CRITICAL debugging message.
*/
void nonBlockingCheck(ACLCB * callback, void *callback_data);

private:
/// Calls non-blocking check callback with the answer and destroys self.
/// If abortReason is provided, sets the final answer to ACCESS_DUNNO.
Expand Down
19 changes: 10 additions & 9 deletions src/acl/FilledChecklist.cc
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,17 @@ ACLFilledChecklist::markSourceDomainChecked()
/*
* There are two common ACLFilledChecklist lifecycles paths:
*
* A) Using aclCheckFast(): The caller creates an ACLFilledChecklist object
* on stack and calls aclCheckFast().
* "Fast" (always synchronous or "blocking"): The user constructs an
* ACLFilledChecklist object on stack, configures it as needed, and calls one
* or both of its fastCheck() methods.
*
* B) Using aclNBCheck() and callbacks: The caller allocates an
* ACLFilledChecklist object (via operator new) and passes it to
* aclNBCheck(). Control eventually passes to ACLChecklist::checkCallback(),
* which will invoke the callback function as requested by the
* original caller of aclNBCheck(). This callback function must
* *not* delete the list. After the callback function returns,
* checkCallback() will delete the list (i.e., self).
* "Slow" (usually asynchronous or "non-blocking"): The user allocates an
* ACLFilledChecklist object on heap (via Make()), configures it as needed,
* and passes it to NonBlockingCheck() while specifying the callback function
* to call with check results. NonBlockingCheck() calls the callback function
* (if the corresponding cbdata is still valid), either immediately/directly
* (XXX) or eventually/asynchronously. After this callback obligations are
* fulfilled, checkCallback() deletes the checklist object (i.e. "this").
*/
ACLFilledChecklist::ACLFilledChecklist(const acl_access *A, HttpRequest *http_request):
dst_rdns(nullptr),
Expand Down
16 changes: 15 additions & 1 deletion src/acl/FilledChecklist.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,27 @@ class ConnStateData;
*/
class ACLFilledChecklist: public ACLChecklist
{
CBDATA_CLASS(ACLFilledChecklist);
CBDATA_CLASS_WITH_MAKE(ACLFilledChecklist);

public:
/// 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.


ACLFilledChecklist();
ACLFilledChecklist(const acl_access *, HttpRequest *);
~ACLFilledChecklist() override;

/// Creates an ACLFilledChecklist object with given constructor arguments.
/// Callers are expected to eventually proceed with NonBlockingCheck().
static MakingPointer Make(const acl_access *a, HttpRequest *r) { return MakingPointer(new ACLFilledChecklist(a, r)); }

/// \copydoc ACLChecklist::nonBlockingCheck()
/// This public nonBlockingCheck() wrapper should be paired with Make(). The
/// pair prevents exception-caused Checklist memory leaks in caller code.
static void NonBlockingCheck(MakingPointer &&p, ACLCB *cb, void *data) { p->nonBlockingCheck(cb, data); (void)p.release(); }

/// configure client request-related fields for the first time
void setRequest(HttpRequest *);

Expand Down
4 changes: 2 additions & 2 deletions src/adaptation/AccessCheck.cc
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,11 @@ Adaptation::AccessCheck::checkCandidates()
while (!candidates.empty()) {
if (AccessRule *r = FindRule(topCandidate())) {
/* BUG 2526: what to do when r->acl is empty?? */
const auto acl_checklist = new ACLFilledChecklist(r->acl, filter.request);
auto acl_checklist = ACLFilledChecklist::Make(r->acl, filter.request);
acl_checklist->updateAle(filter.al);
acl_checklist->updateReply(filter.reply);
acl_checklist->syncAle(filter.request, nullptr);
acl_checklist->nonBlockingCheck(AccessCheckCallbackWrapper, this);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), AccessCheckCallbackWrapper, this);
return;
}

Expand Down
11 changes: 8 additions & 3 deletions src/cbdata.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,12 @@ cbdata_type cbdataInternalAddType(cbdata_type type, const char *label, int size)

/// declaration-generator used internally by CBDATA_CLASS() and CBDATA_CHILD()
#define CBDATA_DECL_(type, methodSpecifiers) \
public: \
void *operator new(size_t size) { \
assert(size == sizeof(type)); \
if (!CBDATA_##type) CBDATA_##type = cbdataInternalAddType(CBDATA_##type, #type, sizeof(type)); \
return (type *)cbdataInternalAlloc(CBDATA_##type); \
} \
public: \
void operator delete (void *address) { \
if (address) cbdataInternalFree(address); \
} \
Expand All @@ -286,12 +286,17 @@ class CbdataParent
/// cbdata-enables a stand-alone class that is not a CbdataParent child
/// sets the class declaration section to "private"
/// use this at the start of your class declaration for consistency sake
#define CBDATA_CLASS(type) CBDATA_DECL_(type, noexcept)
#define CBDATA_CLASS(type) public: CBDATA_DECL_(type, noexcept)

/// A CBDATA_CLASS() variant for classes that want to prevent accidental
/// operator new() calls by making that operator private and forcing external
/// users to call a Make() function instead.
#define CBDATA_CLASS_WITH_MAKE(type) private: CBDATA_DECL_(type, noexcept)

/// cbdata-enables a final CbdataParent-derived class in a hierarchy
/// sets the class declaration section to "private"
/// use this at the start of your class declaration for consistency sake
#define CBDATA_CHILD(type) CBDATA_DECL_(type, final) \
#define CBDATA_CHILD(type) public: CBDATA_DECL_(type, final) \
void finalizedInCbdataChild() final {}

/// cbdata-enables a non-final CbdataParent-derived class T in a hierarchy.
Expand Down
14 changes: 7 additions & 7 deletions src/client_side.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2484,7 +2484,7 @@ ConnStateData::postHttpsAccept()
CodeContext::Reset(connectAle);
// TODO: Use these request/ALE when waiting for new bumped transactions.

const auto acl_checklist = new ACLFilledChecklist(Config.accessList.ssl_bump, request);
auto acl_checklist = ACLFilledChecklist::Make(Config.accessList.ssl_bump, request);
fillChecklist(*acl_checklist);
// Build a local AccessLogEntry to allow requiresAle() acls work
acl_checklist->al = connectAle;
Expand All @@ -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.

#else
fatal("FATAL: SSL-Bump requires --with-openssl");
#endif
Expand Down Expand Up @@ -2967,12 +2967,12 @@ ConnStateData::startPeekAndSplice()
sslServerBump->step = XactionStep::tlsBump2;
// Run a accessList check to check if want to splice or continue bumping

const auto acl_checklist = new ACLFilledChecklist(Config.accessList.ssl_bump, sslServerBump->request.getRaw());
auto acl_checklist = ACLFilledChecklist::Make(Config.accessList.ssl_bump, sslServerBump->request.getRaw());
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.

return;
}

Expand Down Expand Up @@ -3111,7 +3111,7 @@ ConnStateData::initiateTunneledRequest(HttpRequest::Pointer const &cause, const
// TLS handshakes on non-bumping https_port. TODO: Discover these
// problems earlier so that they can be classified/detailed better.
debugs(33, 2, "Not able to compute URL, abort request tunneling for " << reason);
// TODO: throw when nonBlockingCheck() callbacks gain job protections
// TODO: throw when NonBlockingCheck() callbacks gain job protections
static const auto d = MakeNamedErrorDetail("TUNNEL_TARGET");
updateError(ERR_INVALID_REQ, d);
return false;
Expand Down Expand Up @@ -3448,10 +3448,10 @@ varyEvaluateMatch(StoreEntry * entry, HttpRequest * request)
}
}

ACLFilledChecklist *
ACLFilledChecklist::MakingPointer
clientAclChecklistCreate(const acl_access * acl, ClientHttpRequest * http)
{
const auto checklist = new ACLFilledChecklist(acl, nullptr);
auto checklist = ACLFilledChecklist::Make(acl, nullptr);
clientAclChecklistFill(*checklist, http);
return checklist;
}
Expand Down
4 changes: 2 additions & 2 deletions src/client_side_reply.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1835,10 +1835,10 @@ clientReplyContext::processReplyAccess ()
}

/** Process http_reply_access lists */
ACLFilledChecklist *replyChecklist =
auto replyChecklist =
clientAclChecklistCreate(Config.accessList.reply, http);
replyChecklist->updateReply(reply);
replyChecklist->nonBlockingCheck(ProcessReplyAccessResult, this);
ACLFilledChecklist::NonBlockingCheck(std::move(replyChecklist), ProcessReplyAccessResult, this);
}

void
Expand Down
32 changes: 16 additions & 16 deletions src/client_side_request.cc
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,13 @@ clientFollowXForwardedForCheck(Acl::Answer answer, void *data)
if ((addr = asciiaddr)) {
request->indirect_client_addr = addr;
request->x_forwarded_for_iterator.cut(l);
const auto ch = clientAclChecklistCreate(Config.accessList.followXFF, http);
auto ch = clientAclChecklistCreate(Config.accessList.followXFF, http);
if (!Config.onoff.acl_uses_indirect_client) {
/* override the default src_addr tested if we have to go deeper than one level into XFF */
ch->src_addr = request->indirect_client_addr;
}
if (++calloutContext->currentXffHopNumber < SQUID_X_FORWARDED_FOR_HOP_MAX) {
ch->nonBlockingCheck(clientFollowXForwardedForCheck, data);
ACLFilledChecklist::NonBlockingCheck(std::move(ch), clientFollowXForwardedForCheck, data);
return;
}
const auto headerName = Http::HeaderLookupTable.lookup(Http::HdrType::X_FORWARDED_FOR).name;
Expand Down Expand Up @@ -666,15 +666,15 @@ ClientRequestContext::clientAccessCheck()
http->request->x_forwarded_for_iterator = http->request->header.getList(Http::HdrType::X_FORWARDED_FOR);

/* begin by checking to see if we trust direct client enough to walk XFF */
const auto acl_checklist = clientAclChecklistCreate(Config.accessList.followXFF, http);
acl_checklist->nonBlockingCheck(clientFollowXForwardedForCheck, this);
auto acl_checklist = clientAclChecklistCreate(Config.accessList.followXFF, http);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), clientFollowXForwardedForCheck, this);
return;
}
#endif

if (Config.accessList.http) {
const auto acl_checklist = clientAclChecklistCreate(Config.accessList.http, http);
acl_checklist->nonBlockingCheck(clientAccessCheckDoneWrapper, this);
auto acl_checklist = clientAclChecklistCreate(Config.accessList.http, http);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), clientAccessCheckDoneWrapper, this);
} else {
debugs(0, DBG_CRITICAL, "No http_access configuration found. This will block ALL traffic");
clientAccessCheckDone(ACCESS_DENIED);
Expand All @@ -690,8 +690,8 @@ void
ClientRequestContext::clientAccessCheck2()
{
if (Config.accessList.adapted_http) {
const auto acl_checklist = clientAclChecklistCreate(Config.accessList.adapted_http, http);
acl_checklist->nonBlockingCheck(clientAccessCheckDoneWrapper, this);
auto acl_checklist = clientAclChecklistCreate(Config.accessList.adapted_http, http);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), clientAccessCheckDoneWrapper, this);
} else {
debugs(85, 2, "No adapted_http_access configuration. default: ALLOW");
clientAccessCheckDone(ACCESS_ALLOWED);
Expand Down Expand Up @@ -835,8 +835,8 @@ ClientRequestContext::clientRedirectStart()
debugs(33, 5, "'" << http->uri << "'");
http->al->syncNotes(http->request);
if (Config.accessList.redirector) {
const auto acl_checklist = clientAclChecklistCreate(Config.accessList.redirector, http);
acl_checklist->nonBlockingCheck(clientRedirectAccessCheckDone, this);
auto acl_checklist = clientAclChecklistCreate(Config.accessList.redirector, http);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), clientRedirectAccessCheckDone, this);
} else
redirectStart(http, clientRedirectDoneWrapper, this);
}
Expand Down Expand Up @@ -871,8 +871,8 @@ ClientRequestContext::clientStoreIdStart()
debugs(33, 5,"'" << http->uri << "'");

if (Config.accessList.store_id) {
const auto acl_checklist = clientAclChecklistCreate(Config.accessList.store_id, http);
acl_checklist->nonBlockingCheck(clientStoreIdAccessCheckDone, this);
auto acl_checklist = clientAclChecklistCreate(Config.accessList.store_id, http);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), clientStoreIdAccessCheckDone, this);
} else
storeIdStart(http, clientStoreIdDoneWrapper, this);
}
Expand Down Expand Up @@ -1310,8 +1310,8 @@ void
ClientRequestContext::checkNoCache()
{
if (Config.accessList.noCache) {
const auto acl_checklist = clientAclChecklistCreate(Config.accessList.noCache, http);
acl_checklist->nonBlockingCheck(checkNoCacheDoneWrapper, this);
auto acl_checklist = clientAclChecklistCreate(Config.accessList.noCache, http);
ACLFilledChecklist::NonBlockingCheck(std::move(acl_checklist), checkNoCacheDoneWrapper, this);
} else {
/* unless otherwise specified, we try to cache. */
checkNoCacheDone(ACCESS_ALLOWED);
Expand Down Expand Up @@ -1405,8 +1405,8 @@ ClientRequestContext::sslBumpAccessCheck()

debugs(85, 5, "SslBump possible, checking ACL");

ACLFilledChecklist *aclChecklist = clientAclChecklistCreate(Config.accessList.ssl_bump, http);
aclChecklist->nonBlockingCheck(sslBumpAccessCheckDoneWrapper, this);
auto aclChecklist = clientAclChecklistCreate(Config.accessList.ssl_bump, http);
ACLFilledChecklist::NonBlockingCheck(std::move(aclChecklist), sslBumpAccessCheckDoneWrapper, this);
return true;
}

Expand Down
3 changes: 2 additions & 1 deletion src/client_side_request.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#define SQUID_SRC_CLIENT_SIDE_REQUEST_H

#include "AccessLogEntry.h"
#include "acl/FilledChecklist.h"
#include "client_side.h"
#include "clientStream.h"
#include "http/forward.h"
Expand Down Expand Up @@ -257,7 +258,7 @@ class ClientHttpRequest
/* client http based routines */
char *clientConstructTraceEcho(ClientHttpRequest *);

ACLFilledChecklist *clientAclChecklistCreate(const acl_access *, ClientHttpRequest *);
ACLFilledChecklist::MakingPointer clientAclChecklistCreate(const acl_access *, ClientHttpRequest *);
void clientAclChecklistFill(ACLFilledChecklist &, ClientHttpRequest *);
void clientAccessCheck(ClientHttpRequest *);

Expand Down
8 changes: 4 additions & 4 deletions src/peer_select.cc
Original file line number Diff line number Diff line change
Expand Up @@ -613,18 +613,18 @@ PeerSelector::selectMore()
if (always_direct == ACCESS_DUNNO) {
debugs(44, 3, "direct = " << DirectStr[direct] << " (always_direct to be checked)");
/** check always_direct; */
const auto ch = new ACLFilledChecklist(Config.accessList.AlwaysDirect, request);
auto ch = ACLFilledChecklist::Make(Config.accessList.AlwaysDirect, request);
ch->al = al;
ch->syncAle(request, nullptr);
ch->nonBlockingCheck(CheckAlwaysDirectDone, this);
ACLFilledChecklist::NonBlockingCheck(std::move(ch), CheckAlwaysDirectDone, this);
return;
} else if (never_direct == ACCESS_DUNNO) {
debugs(44, 3, "direct = " << DirectStr[direct] << " (never_direct to be checked)");
/** check never_direct; */
const auto ch = new ACLFilledChecklist(Config.accessList.NeverDirect, request);
auto ch = ACLFilledChecklist::Make(Config.accessList.NeverDirect, request);
ch->al = al;
ch->syncAle(request, nullptr);
ch->nonBlockingCheck(CheckNeverDirectDone, this);
ACLFilledChecklist::NonBlockingCheck(std::move(ch), CheckNeverDirectDone, this);
return;
} else if (request->flags.noDirect) {
/** if we are accelerating, direct is not an option. */
Expand Down
4 changes: 2 additions & 2 deletions src/security/PeerConnector.cc
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ Security::PeerConnector::initialize(Security::SessionPointer &serverSession)
// TODO: Remove ACLFilledChecklist::sslErrors and other pre-computed
// state in favor of the ACLs accessing current/fresh info directly.
if (acl_access *acl = ::Config.ssl_client.cert_error) {
const auto check = new ACLFilledChecklist(acl, request.getRaw());
auto check = ACLFilledChecklist::Make(acl, request.getRaw());
fillChecklist(*check);
SSL_set_ex_data(serverSession.get(), ssl_ex_index_cert_error_check, check);
SSL_set_ex_data(serverSession.get(), ssl_ex_index_cert_error_check, check.release());
}
}

Expand Down
Loading
Loading