]> git.ipfire.org Git - thirdparty/squid.git/blob - src/acl/Checklist.cc
Fix detection of concurrent ACLChecklist checks, avoiding !accessList asserts.
[thirdparty/squid.git] / src / acl / Checklist.cc
1 /*
2 * DEBUG: section 28 Access Control
3 */
4
5 #include "squid.h"
6 #include "acl/Checklist.h"
7 #include "acl/Tree.h"
8 #include "Debug.h"
9 #include "profiler/Profiler.h"
10
11 /// common parts of nonBlockingCheck() and resumeNonBlockingCheck()
12 bool
13 ACLChecklist::prepNonBlocking()
14 {
15 assert(accessList);
16
17 if (callerGone()) {
18 checkCallback(ACCESS_DUNNO); // the answer does not really matter
19 return false;
20 }
21
22 /** \par
23 * If the accessList is no longer valid (i.e. its been
24 * freed because of a reconfigure), then bail with ACCESS_DUNNO.
25 */
26
27 if (!cbdataReferenceValid(accessList)) {
28 cbdataReferenceDone(accessList);
29 debugs(28, 4, "ACLChecklist::check: " << this << " accessList is invalid");
30 checkCallback(ACCESS_DUNNO);
31 return false;
32 }
33
34 return true;
35 }
36
37 void
38 ACLChecklist::completeNonBlocking()
39 {
40 assert(!asyncInProgress());
41
42 if (!finished())
43 calcImplicitAnswer();
44
45 cbdataReferenceDone(accessList);
46 checkCallback(currentAnswer());
47 }
48
49 void
50 ACLChecklist::markFinished(const allow_t &finalAnswer, const char *reason)
51 {
52 assert (!finished() && !asyncInProgress());
53 finished_ = true;
54 allow_ = finalAnswer;
55 debugs(28, 3, HERE << this << " answer " << allow_ << " for " << reason);
56 }
57
58 /// Called first (and once) by all checks to initialize their state
59 void
60 ACLChecklist::preCheck(const char *what)
61 {
62 debugs(28, 3, HERE << this << " checking " << what);
63
64 // concurrent checks using the same Checklist are not supported
65 assert(!occupied_);
66 occupied_ = true;
67
68 AclMatchedName = NULL;
69 finished_ = false;
70 }
71
72 bool
73 ACLChecklist::matchChild(const Acl::InnerNode *current, Acl::Nodes::const_iterator pos, const ACL *child)
74 {
75 assert(current && child);
76
77 // Remember the current tree location to prevent "async loop" cases where
78 // the same child node wants to go async more than once.
79 matchLoc_ = Breadcrumb(current, pos);
80
81 // if there are any breadcrumbs left, then follow them on the way down
82 bool result = false;
83 if (matchPath.empty()) {
84 result = child->matches(this);
85 } else {
86 const Breadcrumb top(matchPath.top());
87 assert(child == top.parent);
88 matchPath.pop();
89 result = top.parent->resumeMatchingAt(this, top.position);
90 }
91
92 if (asyncInProgress()) {
93 // We get here for node N that called goAsync() and then, as the call
94 // stack unwinds, for the nodes higher in the ACL tree that led to N.
95 matchPath.push(Breadcrumb(current, pos));
96 } else {
97 asyncLoc_.clear();
98 }
99
100 matchLoc_.clear();
101 return result;
102 }
103
104 bool
105 ACLChecklist::goAsync(AsyncState *state)
106 {
107 assert(state);
108 assert(!asyncInProgress());
109 assert(matchLoc_.parent);
110
111 // TODO: add a once-in-a-while WARNING about fast directive using slow ACL?
112 if (!asyncCaller_) {
113 debugs(28, 2, this << " a fast-only directive uses a slow ACL!");
114 return false;
115 }
116
117 // TODO: add a once-in-a-while WARNING about async loops?
118 if (matchLoc_ == asyncLoc_) {
119 debugs(28, 2, this << " a slow ACL resumes by going async again!");
120 return false;
121 }
122
123 asyncLoc_ = matchLoc_; // prevent async loops
124
125 asyncStage_ = asyncStarting;
126 changeState(state);
127 state->checkForAsync(this); // this is supposed to go async
128
129 // Did AsyncState object actually go async? If not, tell the caller.
130 if (asyncStage_ != asyncStarting) {
131 assert(asyncStage_ == asyncFailed);
132 asyncStage_ = asyncNone; // sanity restored
133 return false;
134 }
135
136 // yes, we must pause until the async callback calls resumeNonBlockingCheck
137 asyncStage_ = asyncRunning;
138 return true;
139 }
140
141 // ACLFilledChecklist overwrites this to unclock something before we
142 // "delete this"
143 void
144 ACLChecklist::checkCallback(allow_t answer)
145 {
146 ACLCB *callback_;
147 void *cbdata_;
148 debugs(28, 3, "ACLChecklist::checkCallback: " << this << " answer=" << answer);
149
150 callback_ = callback;
151 callback = NULL;
152
153 if (cbdataReferenceValidDone(callback_data, &cbdata_))
154 callback_(answer, cbdata_);
155
156 // not really meaningful just before delete, but here for completeness sake
157 occupied_ = false;
158
159 delete this;
160 }
161
162 ACLChecklist::ACLChecklist() :
163 accessList (NULL),
164 callback (NULL),
165 callback_data (NULL),
166 asyncCaller_(false),
167 occupied_(false),
168 finished_(false),
169 allow_(ACCESS_DENIED),
170 asyncStage_(asyncNone),
171 state_(NullState::Instance())
172 {
173 }
174
175 ACLChecklist::~ACLChecklist()
176 {
177 assert (!asyncInProgress());
178
179 cbdataReferenceDone(accessList);
180
181 debugs(28, 4, "ACLChecklist::~ACLChecklist: destroyed " << this);
182 }
183
184 ACLChecklist::NullState *
185 ACLChecklist::NullState::Instance()
186 {
187 return &_instance;
188 }
189
190 void
191 ACLChecklist::NullState::checkForAsync(ACLChecklist *) const
192 {
193 assert(false); // or the Checklist will never get out of the async state
194 }
195
196 ACLChecklist::NullState ACLChecklist::NullState::_instance;
197
198 void
199 ACLChecklist::changeState (AsyncState *newState)
200 {
201 /* only change from null to active and back again,
202 * not active to active.
203 * relax this once conversion to states is complete
204 * RBC 02 2003
205 */
206 assert (state_ == NullState::Instance() || newState == NullState::Instance());
207 state_ = newState;
208 }
209
210 ACLChecklist::AsyncState *
211 ACLChecklist::asyncState() const
212 {
213 return state_;
214 }
215
216 /**
217 * Kick off a non-blocking (slow) ACL access list test
218 *
219 * NP: this should probably be made Async now.
220 */
221 void
222 ACLChecklist::nonBlockingCheck(ACLCB * callback_, void *callback_data_)
223 {
224 preCheck("slow rules");
225 callback = callback_;
226 callback_data = cbdataReference(callback_data_);
227 asyncCaller_ = true;
228
229 /** The ACL List should NEVER be NULL when calling this method.
230 * Always caller should check for NULL and handle appropriate to its needs first.
231 * We cannot select a sensible default for all callers here. */
232 if (accessList == NULL) {
233 debugs(28, DBG_CRITICAL, "SECURITY ERROR: ACL " << this << " checked with nothing to match against!!");
234 checkCallback(ACCESS_DUNNO);
235 return;
236 }
237
238 if (prepNonBlocking()) {
239 matchAndFinish(); // calls markFinished() on success
240 if (!asyncInProgress())
241 completeNonBlocking();
242 } // else checkCallback() has been called
243 }
244
245 void
246 ACLChecklist::resumeNonBlockingCheck(AsyncState *state)
247 {
248 assert(asyncState() == state);
249 changeState(NullState::Instance());
250
251 if (asyncStage_ == asyncStarting) { // oops, we did not really go async
252 asyncStage_ = asyncFailed; // goAsync() checks for that
253 // Do not fall through to resume checks from the async callback. Let
254 // the still-pending(!) goAsync() notice and notify its caller instead.
255 return;
256 }
257 assert(asyncStage_ == asyncRunning);
258 asyncStage_ = asyncNone;
259
260 assert(!matchPath.empty());
261
262 if (!prepNonBlocking())
263 return; // checkCallback() has been called
264
265 if (!finished())
266 matchAndFinish();
267
268 if (asyncInProgress())
269 assert(!matchPath.empty()); // we have breadcrumbs to resume matching
270 else
271 completeNonBlocking();
272 }
273
274 /// performs (or resumes) an ACL tree match and, if successful, sets the action
275 void
276 ACLChecklist::matchAndFinish()
277 {
278 bool result = false;
279 if (matchPath.empty()) {
280 result = accessList->matches(this);
281 } else {
282 const Breadcrumb top(matchPath.top());
283 matchPath.pop();
284 result = top.parent->resumeMatchingAt(this, top.position);
285 }
286
287 if (result) // the entire tree matched
288 markFinished(accessList->winningAction(), "match");
289 }
290
291 allow_t const &
292 ACLChecklist::fastCheck(const Acl::Tree * list)
293 {
294 PROF_start(aclCheckFast);
295
296 preCheck("fast ACLs");
297 asyncCaller_ = false;
298
299 // Concurrent checks are not supported, but sequential checks are, and they
300 // may use a mixture of fastCheck(void) and fastCheck(list) calls.
301 const Acl::Tree * const savedList = accessList;
302
303 accessList = cbdataReference(list);
304
305 // assume DENY/ALLOW on mis/matches due to action-free accessList
306 // matchAndFinish() takes care of the ALLOW case
307 if (accessList && cbdataReferenceValid(accessList))
308 matchAndFinish(); // calls markFinished() on success
309 if (!finished())
310 markFinished(ACCESS_DENIED, "ACLs failed to match");
311
312 cbdataReferenceDone(accessList);
313 accessList = savedList;
314 occupied_ = false;
315 PROF_stop(aclCheckFast);
316 return currentAnswer();
317 }
318
319 /* Warning: do not cbdata lock this here - it
320 * may be static or on the stack
321 */
322 allow_t const &
323 ACLChecklist::fastCheck()
324 {
325 PROF_start(aclCheckFast);
326
327 preCheck("fast rules");
328 asyncCaller_ = false;
329
330 debugs(28, 5, "aclCheckFast: list: " << accessList);
331 const Acl::Tree *acl = cbdataReference(accessList);
332 if (acl != NULL && cbdataReferenceValid(acl)) {
333 matchAndFinish(); // calls markFinished() on success
334
335 // if finished (on a match or in exceptional cases), stop
336 if (finished()) {
337 cbdataReferenceDone(acl);
338 occupied_ = false;
339 PROF_stop(aclCheckFast);
340 return currentAnswer();
341 }
342
343 // fall through for mismatch handling
344 }
345
346 // There were no rules to match or no rules matched
347 calcImplicitAnswer();
348 cbdataReferenceDone(acl);
349 occupied_ = false;
350 PROF_stop(aclCheckFast);
351
352 return currentAnswer();
353 }
354
355 /// When no rules matched, the answer is the inversion of the last rule
356 /// action (or ACCESS_DUNNO if the reversal is not possible).
357 void
358 ACLChecklist::calcImplicitAnswer()
359 {
360 const allow_t lastAction = (accessList && cbdataReferenceValid(accessList)) ?
361 accessList->lastAction() : allow_t(ACCESS_DUNNO);
362 allow_t implicitRuleAnswer = ACCESS_DUNNO;
363 if (lastAction == ACCESS_DENIED) // reverse last seen "deny"
364 implicitRuleAnswer = ACCESS_ALLOWED;
365 else if (lastAction == ACCESS_ALLOWED) // reverse last seen "allow"
366 implicitRuleAnswer = ACCESS_DENIED;
367 // else we saw no rules and will respond with ACCESS_DUNNO
368
369 debugs(28, 3, HERE << this << " NO match found, last action " <<
370 lastAction << " so returning " << implicitRuleAnswer);
371 markFinished(implicitRuleAnswer, "implicit rule won");
372 }
373
374 bool
375 ACLChecklist::callerGone()
376 {
377 return !cbdataReferenceValid(callback_data);
378 }