Hello, The attached patch prevents asserts when Squid reuses the same Checklist object for multiple ACL checks. I missed one use case when adding Checklist reuse controls for trunk r12859 (Boolean ACLs). The bug can be triggered by a combination of log_access and access_log ACLs, for example.
Concurrent checks are not supported, but it is possible for the same ACLChecklist to be used for a sequence of checks, alternating fastCheck(void) and fastCheck(list) calls. We needed a different/dedicated mechanism to detect check concurrency (added ACLChecklist::occupied_), and we needed to preserve (and then restore) pre-set accessList during fastCheck(list) checks. HTH, Alex.
Fix detection of concurrent ACLChecklist checks, avoiding !accessList asserts. Concurrent checks are not supported, but it is possible for the same ACLChecklist to be used for a sequence of checks, alternating fastCheck(void) and fastCheck(list) calls. We needed a different/dedicated mechanism to detect check concurrency (added ACLChecklist::occupied_), and we needed to preserve (and then restore) pre-set accessList during fastCheck(list) checks. === modified file 'src/acl/Checklist.cc' --- src/acl/Checklist.cc 2013-06-01 10:01:13 +0000 +++ src/acl/Checklist.cc 2013-06-07 16:47:47 +0000 @@ -43,40 +43,45 @@ calcImplicitAnswer(); cbdataReferenceDone(accessList); checkCallback(currentAnswer()); } void ACLChecklist::markFinished(const allow_t &finalAnswer, const char *reason) { assert (!finished() && !asyncInProgress()); finished_ = true; allow_ = finalAnswer; debugs(28, 3, HERE << this << " answer " << allow_ << " for " << reason); } /// Called first (and once) by all checks to initialize their state void ACLChecklist::preCheck(const char *what) { debugs(28, 3, HERE << this << " checking " << what); + + // concurrent checks using the same Checklist are not supported + assert(!occupied_); + occupied_ = true; + AclMatchedName = NULL; finished_ = false; } bool ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const ACL *child) { assert(current && child); // Remember the current tree location to prevent "async loop" cases where // the same child node wants to go async more than once. matchLoc_ = Breadcrumb(current, pos); // if there are any breadcrumbs left, then follow them on the way down bool result = false; if (matchPath.empty()) { result = child->matches(this); } else { const Breadcrumb top(matchPath.top()); assert(child == top.parent); @@ -131,48 +136,52 @@ // yes, we must pause until the async callback calls resumeNonBlockingCheck asyncStage_ = asyncRunning; return true; } // ACLFilledChecklist overwrites this to unclock something before we // "delete this" void ACLChecklist::checkCallback(allow_t answer) { ACLCB *callback_; void *cbdata_; debugs(28, 3, "ACLChecklist::checkCallback: " << this << " answer=" << answer); callback_ = callback; callback = NULL; if (cbdataReferenceValidDone(callback_data, &cbdata_)) callback_(answer, cbdata_); + // not really meaningful just before delete, but here for completeness sake + occupied_ = false; + delete this; } ACLChecklist::ACLChecklist() : accessList (NULL), callback (NULL), callback_data (NULL), asyncCaller_(false), + occupied_(false), finished_(false), allow_(ACCESS_DENIED), asyncStage_(asyncNone), state_(NullState::Instance()) { } ACLChecklist::~ACLChecklist() { assert (!asyncInProgress()); cbdataReferenceDone(accessList); debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this); } ACLChecklist::NullState * ACLChecklist::NullState::Instance() { return &_instance; @@ -270,87 +279,91 @@ if (matchPath.empty()) { result = accessList->matches(this); } else { const Breadcrumb top(matchPath.top()); matchPath.pop(); result = top.parent->resumeMatchingAt(this, top.position); } if (result) // the entire tree matched markFinished(accessList->winningAction(), "match"); } allow_t const & ACLChecklist::fastCheck(const Acl::Tree * list) { PROF_start(aclCheckFast); preCheck("fast ACLs"); asyncCaller_ = false; - // This call is not compatible with a pre-set accessList because we cannot - // tell whether this Checklist is used by some other concurent call, which - // is not supported. - assert(!accessList); + // Concurrent checks are not supported, but sequential checks are, and they + // may use a mixture of fastCheck(void) and fastCheck(list) calls. + const Acl::Tree * const savedList = accessList; + accessList = cbdataReference(list); // assume DENY/ALLOW on mis/matches due to action-free accessList // matchAndFinish() takes care of the ALLOW case if (accessList && cbdataReferenceValid(accessList)) matchAndFinish(); // calls markFinished() on success if (!finished()) markFinished(ACCESS_DENIED, "ACLs failed to match"); cbdataReferenceDone(accessList); + accessList = savedList; + occupied_ = false; PROF_stop(aclCheckFast); return currentAnswer(); } /* Warning: do not cbdata lock this here - it * may be static or on the stack */ allow_t const & ACLChecklist::fastCheck() { PROF_start(aclCheckFast); preCheck("fast rules"); asyncCaller_ = false; debugs(28, 5, "aclCheckFast: list: " << accessList); const Acl::Tree *acl = cbdataReference(accessList); if (acl != NULL && cbdataReferenceValid(acl)) { matchAndFinish(); // calls markFinished() on success // if finished (on a match or in exceptional cases), stop if (finished()) { cbdataReferenceDone(acl); + occupied_ = false; PROF_stop(aclCheckFast); return currentAnswer(); } // fall through for mismatch handling } // There were no rules to match or no rules matched calcImplicitAnswer(); cbdataReferenceDone(acl); + occupied_ = false; PROF_stop(aclCheckFast); return currentAnswer(); } /// When no rules matched, the answer is the inversion of the last rule /// action (or ACCESS_DUNNO if the reversal is not possible). void ACLChecklist::calcImplicitAnswer() { const allow_t lastAction = (accessList && cbdataReferenceValid(accessList)) ? accessList->lastAction() : allow_t(ACCESS_DUNNO); allow_t implicitRuleAnswer = ACCESS_DUNNO; if (lastAction == ACCESS_DENIED) // reverse last seen "deny" implicitRuleAnswer = ACCESS_ALLOWED; else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow" implicitRuleAnswer = ACCESS_DENIED; // else we saw no rules and will respond with ACCESS_DUNNO debugs(28, 3, HERE << this << " NO match found, last action " << === modified file 'src/acl/Checklist.h' --- src/acl/Checklist.h 2013-05-28 14:28:15 +0000 +++ src/acl/Checklist.h 2013-06-07 16:47:47 +0000 @@ -207,36 +207,37 @@ Breadcrumb(): parent(NULL) {} Breadcrumb(const Acl::InnerNode *aParent, Acl::Nodes::const_iterator aPos): parent(aParent), position(aPos) {} bool operator ==(const Breadcrumb &b) const { return parent == b.parent && (!parent || position == b.position); } bool operator !=(const Breadcrumb &b) const { return !this->operator ==(b); } void clear() { parent = NULL; } const Acl::InnerNode *parent; ///< intermediate node in the ACL tree Acl::Nodes::const_iterator position; ///< child position inside parent }; /// possible outcomes when trying to match a single ACL node in a list typedef enum { nmrMatch, nmrMismatch, nmrFinished, nmrNeedsAsync } NodeMatchingResult; /// prepare for checking ACLs; called once per check void preCheck(const char *what); bool prepNonBlocking(); void completeNonBlocking(); void calcImplicitAnswer(); bool asyncCaller_; ///< whether the caller supports async/slow ACLs + bool occupied_; ///< whether a check (fast or non-blocking) is in progress bool finished_; allow_t allow_; enum AsyncStage { asyncNone, asyncStarting, asyncRunning, asyncFailed }; AsyncStage asyncStage_; AsyncState *state_; Breadcrumb matchLoc_; ///< location of the node running matches() now Breadcrumb asyncLoc_; ///< currentNode_ that called goAsync() bool callerGone(); /// suspended (due to an async lookup) matches() in the ACL tree std::stack<Breadcrumb> matchPath; }; #endif /* SQUID_ACLCHECKLIST_H */