1 /* Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.
2 
3    This program is free software; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License, version 2.0,
5    as published by the Free Software Foundation.
6 
7    This program is also distributed with certain software (including
8    but not limited to OpenSSL) that is licensed under separate terms,
9    as designated in a particular file or component or in included license
10    documentation.  The authors of MySQL hereby grant you an additional
11    permission to link the program and your derivative works with the
12    separately licensed software that they have included with MySQL.
13 
14    This program is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty of
16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17    GNU General Public License, version 2.0, for more details.
18 
19    You should have received a copy of the GNU General Public License
20    along with this program; if not, write to the Free Software
21    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA */
22 
23 /**
24   services: log filter: basic filtering
25 
26   This implementation, "dragnet" is currently the default filter
27   and therefore built-in.  Basic configuration is built into the
28   server proper (via log_error_verbosity etc.); for advanced configuration,
29   load the service log_filter_dragnet which implements a configuration
30   language for this engine.  See there for details about the Configuration
31   Stage.  Some of the code-paths are only available via the configuration
32   language or an equivalent service (but not without any such service loaded).
33 
34   At present, the design is such that multiple threads can call the
35   filter concurrently; the ruleset is global and shared between all
36   users.
37 
38 
39   FILTERING STAGE
40 
41   At run time, the filter iterates over its rule-set.  For each
42   rule, if the condition contains a well-known item, it looks for
43   an item of that type in the event.  If the condition contains
44   an ad hoc-item, it looks for an item of any ad hoc-type with
45   the given key within the event.
46 
47   If there is a match, the filter will verify whether the storage
48   class of the value in the event and that in the condition are
49   either both strings, or both not.  If the classes do not match,
50   it flags an error.  Otherwise, it now compares both values using
51   the requested comparator, and reports the result.
52 
53   If a log event matches a rule, an action ("suppress log line",
54   "delete field", etc.) will be applied to that event.
55 
56 
57   LOCKING
58 
59   During the filtering stage, a shared lock on the ruleset is held.
60   An exclusive lock on the ruleset is only taken as response to the
61   user's changing of the filter configuration, which should be rare.
62 
63   For debugging puroposes, rules feature a counter of how often events
64   matched them; this counter is updated atomically.
65 
66   Rate-limiting ("throttle") needs some bookkeeping data (when does
67   the current window expire? how many matches have we had so far
68   within the current window? etc.). A write-lock is taken on the
69   individual rule (not the entire ruleset) to update this information;
70   any throttling-related actions taken on the event happen after this
71   lock has been released.
72 
73   The event itself is not locked.
74 */
75 
76 #include <mysqld_error.h>
77 
78 #include "log_builtins_filter_imp.h"
79 #include "log_builtins_imp.h"
80 #include "my_atomic.h"
81 #include "my_systime.h"  // my_micro_time()
82 #include "mysys_err.h"   // EE_ERROR_LAST for globerrs
83 #include "sql/derror.h"
84 #include "sql/log.h"
85 // for the default rules
86 #include "sql/mysqld.h"
87 
88 #define THROTTLE_DEFAULT_WINDOW_SIZE_IN_SECONDS 60
89 #define THROTTLE_MICROSECOND_MULTIPLIER 1000000
90 
91 static bool filter_inited = false;
92 static ulong filter_rule_uuid = 0;
93 
94 log_filter_ruleset *log_filter_builtin_rules = nullptr;
95 log_filter_tag rule_tag_builtin = {"log_filter_builtin", nullptr};
96 
97 /**
98   Predicate: can we add any more rules?
99 
100   @param  rs     the ruleset to check
101 
102   @retval true   full, no more rules can be added
103   @retval false  not full, further rules can be added
104 */
log_filter_ruleset_full(log_filter_ruleset * rs)105 static bool log_filter_ruleset_full(log_filter_ruleset *rs) {
106   return (rs->count >= LOG_FILTER_RULE_MAX);
107 }
108 
109 /**
110   Initialize a new rule.
111 
112   This clears the first unused rule. It does not update the rules
113   count; this is for the caller to do if it succeeds in setting up
114   the rule to its satisfaction. If the caller fails, it should
115   log_builtins_filter_rule_free() the incomplete rule.
116 
117   @retval  nullptr: could not initialize rule. Do not call rule_free.
118   @retval !nullptr: the address of the rule. fill in. on success,
119                     caller must increase rule count.  on failure,
120                     it must call rule_free.
121 */
log_builtins_filter_rule_init(log_filter_ruleset * ruleset)122 static log_filter_rule *log_builtins_filter_rule_init(
123     log_filter_ruleset *ruleset) {
124   log_filter_rule *r = &ruleset->rule[ruleset->count];
125 
126   memset(r, 0, sizeof(log_filter_rule));
127 
128   r->id = ++filter_rule_uuid;
129   r->throttle_window_size =
130       THROTTLE_DEFAULT_WINDOW_SIZE_IN_SECONDS;  // 1 minute
131 
132   if (mysql_rwlock_init(0, &(r->rule_lock))) return nullptr;
133 
134   return r;
135 }
136 
137 /**
138   Release all resources associated with a filter rule.
139   Leaves a "gap" (an uninitialized rule) for immediate re-filling;
140   if this is undesired, use log_builtins_filter_rule_remove() (see there).
141   Must hold rule-set lock.
142 
143   @param  ri  the rule to release
144 
145   @retval     the return value from mysql_rwlock_destroy()
146 */
log_builtins_filter_rule_free(log_filter_rule * ri)147 static int log_builtins_filter_rule_free(log_filter_rule *ri) {
148   ri->cond = LOG_FILTER_COND_NONE;
149   ri->verb = LOG_FILTER_UNDEF;
150 
151   // release memory if needed
152   log_item_free(&(ri->match));
153   log_item_free(&(ri->aux));
154 
155   return mysql_rwlock_destroy(&(ri->rule_lock));
156 }
157 
158 /**
159   Release filter rule (key/value pair) with the index "elem" in "ruleset".
160   This frees whichever of key and value were dynamically allocated.
161   It then moves any trailing items to fill the "gap" and decreases the
162   counter of elements in the rule-set.
163 
164   If the intention is to leave a "gap" in the bag that may immediately be
165   overwritten with an updated element, use log_builtins_filter_rule_free()
166   instead.
167 
168   Caller must hold rule-set lock.
169 
170   @param         ruleset   filter rule-set
171   @param         elem      index of the filter rule to release
172 */
log_builtins_filter_rule_remove(log_filter_ruleset * ruleset,int elem)173 static void log_builtins_filter_rule_remove(log_filter_ruleset *ruleset,
174                                             int elem) {
175   size_t rn;
176 
177   DBUG_ASSERT(ruleset->count > 0);
178 
179   log_builtins_filter_rule_free(&ruleset->rule[elem]);
180 
181   for (rn = elem; rn < (ruleset->count - 1); rn++) {
182     ruleset->rule[rn] = ruleset->rule[rn + 1];
183   }
184 
185   ruleset->count--;
186 }
187 
188 /**
189    Create a new set of filter rules.
190 
191    @param   tag       tag for this ruleset
192    @param   count     number of rules to allocate, 0 for default
193 
194    @retval            a pointer to a ruleset structure, or nullptr on failure
195 */
log_builtins_filter_ruleset_new(log_filter_tag * tag,size_t count)196 static log_filter_ruleset *log_builtins_filter_ruleset_new(log_filter_tag *tag,
197                                                            size_t count) {
198   log_filter_ruleset *ruleset = nullptr;
199 
200   if ((tag == nullptr) || (tag->filter_name == nullptr))
201     return nullptr; /* purecov: inspected */
202 
203   ruleset =
204       (log_filter_ruleset *)my_malloc(0, sizeof(log_filter_ruleset), MYF(0));
205 
206   if (ruleset != nullptr) {
207     memset(ruleset, 0, sizeof(log_filter_ruleset));
208     ruleset->tag = tag;
209     ruleset->alloc = (count < 1) ? LOG_FILTER_RULE_MAX : count;
210 
211     if (mysql_rwlock_init(0, &ruleset->ruleset_lock)) {
212       my_free((void *)ruleset); /* purecov: inspected */
213       ruleset = nullptr;        /* purecov: inspected */
214     }
215   }
216 
217   return ruleset;
218 }
219 
220 /**
221   Lock and get the filter rules.
222 
223   @param  ruleset  the ruleset to lock
224   @param  lt       LOG_BUILTINS_LOCK_SHARED     lock for reading
225                    LOG_BUILTINS_LOCK_EXCLUSIVE  lock for writing
226 
227   @retval  0       lock acquired
228   @retval !0       failed to acquire lock
229 */
log_builtins_filter_ruleset_lock(log_filter_ruleset * ruleset,log_builtins_filter_lock lt)230 static int log_builtins_filter_ruleset_lock(log_filter_ruleset *ruleset,
231                                             log_builtins_filter_lock lt) {
232   if ((!filter_inited) || (ruleset == nullptr)) return -1;
233 
234   switch (lt) {
235     case LOG_BUILTINS_LOCK_SHARED:
236       mysql_rwlock_rdlock(&ruleset->ruleset_lock);
237       break;
238     case LOG_BUILTINS_LOCK_EXCLUSIVE:
239       mysql_rwlock_wrlock(&ruleset->ruleset_lock);
240       break;
241     default:
242       return -2; /* purecov: inspected */
243   }
244 
245   return 0;
246 }
247 
248 /**
249   Drop an entire filter rule-set. Must hold lock.
250 */
log_builtins_filter_ruleset_drop(log_filter_ruleset * ruleset)251 static void log_builtins_filter_ruleset_drop(log_filter_ruleset *ruleset) {
252   log_filter_rule *ri;
253 
254   while (ruleset->count > 0) {
255     ruleset->count--;
256     ri = &ruleset->rule[ruleset->count];
257     log_builtins_filter_rule_free(ri);
258   }
259 }
260 
261 /**
262   Unlock filter ruleset.
263 */
log_builtins_filter_ruleset_unlock(log_filter_ruleset * ruleset)264 static void log_builtins_filter_ruleset_unlock(log_filter_ruleset *ruleset) {
265   if (ruleset != nullptr) {
266     mysql_rwlock_unlock(&(ruleset->ruleset_lock));
267   }
268 }
269 
270 /**
271   Free filter ruleset.
272 */
log_builtins_filter_ruleset_free(log_filter_ruleset ** ruleset)273 static void log_builtins_filter_ruleset_free(log_filter_ruleset **ruleset) {
274   if (ruleset != nullptr) {
275     log_filter_ruleset *rs = *ruleset;
276     if (rs != nullptr) {
277       *ruleset = nullptr;
278 
279       log_builtins_filter_ruleset_drop(rs);
280       log_builtins_filter_ruleset_unlock(rs);
281 
282       mysql_rwlock_destroy(&(rs->ruleset_lock));
283 
284       my_free((void *)rs);
285     }
286   }
287 }
288 
289 /**
290   Defaults for when the configuration engine isn't loaded;
291   aim for 5.7 compatibilty.
292 */
log_builtins_filter_set_defaults(log_filter_ruleset * ruleset)293 static void log_builtins_filter_set_defaults(log_filter_ruleset *ruleset) {
294   log_filter_rule *r;
295 
296   DBUG_ASSERT(ruleset != nullptr);
297 
298   DBUG_ASSERT(!log_filter_ruleset_full(ruleset));
299 
300   // failsafe: simple built-in filter may not drop "System" or "Error" messages
301   r = log_builtins_filter_rule_init(ruleset);
302   log_item_set_with_key(&r->match, LOG_ITEM_LOG_PRIO, nullptr,
303                         LOG_ITEM_FREE_NONE)
304       ->data_integer = WARNING_LEVEL;
305   r->cond = LOG_FILTER_COND_LT;
306   r->verb = LOG_FILTER_RETURN;
307 
308   ruleset->count++;
309 
310   // sys_var: log_error_verbosity
311   r = log_builtins_filter_rule_init(ruleset);
312   log_item_set_with_key(&r->match, LOG_ITEM_LOG_PRIO, nullptr,
313                         LOG_ITEM_FREE_NONE)
314       ->data_integer = log_error_verbosity;
315   r->cond = LOG_FILTER_COND_GT;
316   r->verb = LOG_FILTER_DROP;
317 
318   ruleset->count++;
319 
320   // example: remove all source-line log items
321   // these are not desirable by default, only while debugging.
322   r = log_builtins_filter_rule_init(ruleset);
323   log_item_set(&r->match, LOG_ITEM_SRC_LINE);
324   r->cond = LOG_FILTER_COND_PRESENT;
325   r->verb = LOG_FILTER_ITEM_DEL;
326   // aux optional
327 
328   ruleset->count++;
329 }
330 
331 /**
332   Deinitialize filtering engine.
333 
334   @retval  0   Success!
335   @retval -1   De-initialize? Filter wasn't even initialized!
336 */
log_builtins_filter_exit()337 int log_builtins_filter_exit() {
338   if (!filter_inited) return -1;
339 
340   /*
341     Nobody else should run at this point anyway, but
342     since we made a big song and dance about having
343     to hold this lock above ...
344   */
345   if (log_filter_builtin_rules != nullptr) {
346     mysql_rwlock_wrlock(&log_filter_builtin_rules->ruleset_lock);
347     filter_inited = false;
348     log_builtins_filter_ruleset_free(&log_filter_builtin_rules);
349   } else
350     filter_inited = false; /* purecov: inspected */
351 
352   return 0;
353 }
354 
355 /**
356   Initialize filtering engine.
357   We need to do this early, before the component system is up.
358 
359   @retval  0   Success!
360   @retval -1   Couldn't initialize ruleset
361   @retval -2   Filter was already initialized?
362 */
log_builtins_filter_init()363 int log_builtins_filter_init() {
364   if (!filter_inited) {
365     log_filter_builtin_rules =
366         log_builtins_filter_ruleset_new(&rule_tag_builtin, 0);
367     if (log_filter_builtin_rules == nullptr) return -1;
368 
369     log_builtins_filter_set_defaults(log_filter_builtin_rules);
370     filter_inited = true;
371 
372     return 0;
373   } else
374     return -2;
375 }
376 
377 /**
378   Apply the action of an individual rule to an individual log line (or
379   a part thereof, i.e. a "field"). At this point, we already know that
380   the current log line matches the condition.
381 
382   @param[in,out]   ll                   the current log line
383   @param           ln                   index of the matching field,
384                                         -1 for none (when a test for absence
385                                         matched)
386   @param           r                    the rule to apply. internal state may
387                                         be changed (i.e. the number of seen
388                                         matches for a throttle rule)
389 
390   @retval          log_filter_apply     0 on success, an error-code otherwise
391 */
392 
log_filter_try_apply(log_line * ll,int ln,log_filter_rule * r)393 static log_filter_apply log_filter_try_apply(log_line *ll, int ln,
394                                              log_filter_rule *r) {
395   switch (r->verb) {
396     case LOG_FILTER_RETURN:
397       break;
398 
399     case LOG_FILTER_DROP:
400       log_line_item_free_all(ll);
401       break;
402 
403     case LOG_FILTER_THROTTLE: {
404       ulonglong now = my_micro_time();
405       ulong rate = (ulong)(
406           (r->aux.data.data_integer < 0) ? 0 : r->aux.data.data_integer);
407       ulong suppressed = 0;
408       ulong matches;
409 
410       /*
411         Check whether we're still in the current window. (If not, we
412         will want to print a summary (if the logging of any lines was
413         suppressed) and start a new window.)
414       */
415       mysql_rwlock_wrlock(&(r->rule_lock));
416 
417       if (now >= r->throttle_window_end) {
418         suppressed =
419             (r->throttle_matches > rate) ? (r->throttle_matches - rate) : 0;
420 
421         // new window
422         r->throttle_matches = 0;
423         r->throttle_window_end =
424             now + (r->throttle_window_size * THROTTLE_MICROSECOND_MULTIPLIER);
425       }
426 
427       matches = ++r->throttle_matches;
428 
429       mysql_rwlock_unlock(&(r->rule_lock));
430 
431       /*
432         If it's over the limit for the current window, discard the whole
433         log line.  A rate of 0 ("discard all") is allowed for convenience,
434         but the DROP verb should be used instead.
435       */
436       if (matches > rate) {
437         log_line_item_free_all(ll);
438       }
439 
440       // if we actually suppressed any lines, add info declaring that
441       else if ((suppressed > 0) && !log_line_full(ll)) {
442         log_line_item_set(ll, LOG_ITEM_LOG_SUPPRESSED)->data_integer =
443             suppressed;
444       }
445     } break;
446 
447     case LOG_FILTER_ITEM_SET:
448       if (r->aux.key == nullptr) return LOG_FILTER_APPLY_TARGET_NOT_IN_LOG_LINE;
449 
450       // if not same field as in match, look for it
451       if ((ln < 0) || (0 != native_strcasecmp(r->aux.key, ll->item[ln].key)))
452         ln = log_line_index_by_item(ll, &r->aux);
453 
454       if (ln >= 0)  // found? release for over-write!
455         log_item_free(&ll->item[ln]);
456       else if (log_line_full(ll))  // otherwise, add it if there's still space
457         return LOG_FILTER_APPLY_OUT_OF_MEMORY;
458       else
459         ln = ll->count++;
460 
461       ll->item[ln] = r->aux;
462       /*
463         It's a shallow copy, don't try to free it.
464         As a downside, the filter rules must remain shared-locked until
465         the line is logged. The assumption is that logging happens vastly
466         more than changing of the ruleset.
467       */
468       ll->item[ln].alloc = LOG_ITEM_FREE_NONE;
469       ll->seen |= r->aux.type;
470       break;
471 
472     case LOG_FILTER_ITEM_DEL:
473       // might want to delete a field other than that from the cond:
474       if ((r->aux.key != nullptr) &&
475           ((ln < 0) ||
476            (0 != native_strcasecmp(r->aux.key, ll->item[ln].key)))) {
477         ln = log_line_index_by_item(ll, &r->aux);
478       }
479 
480       if (ln < 0) return LOG_FILTER_APPLY_TARGET_NOT_IN_LOG_LINE;
481 
482       {
483         log_item_type t = ll->item[ln].type;
484 
485         log_line_item_remove(ll, ln);
486 
487         /*
488           If it's a well-known type (and therefore unique), or if it's
489           the last one of a generic type, unflag the presence of that type.
490         */
491         if (!log_item_generic_type(t) || (log_line_index_by_type(ll, t) < 0))
492           ll->seen &= ~t;
493       }
494 
495       break;
496 
497     default:
498       return LOG_FILTER_APPLY_UNKNOWN_OPERATION;
499   }
500 
501   return LOG_FILTER_APPLY_SUCCESS;
502 }
503 
504 /**
505   Try to match an individual log line-field against an individual
506   rule's condition
507 
508   @param           li                   the log item we try to match
509   @param           ri                   the rule containing the condition
510 
511   @retval          log_filter_match     0 (LOG_FILTER_MATCH_SUCCESS) on match,
512                                         1 (LOG_FILTER_MATCH_UNSATISFIED) or
513                                         an error-code otherwise
514 */
log_filter_try_match(log_item * li,log_filter_rule * ri)515 static log_filter_match log_filter_try_match(log_item *li,
516                                              log_filter_rule *ri) {
517   bool rc, lc;
518   log_filter_match e = LOG_FILTER_MATCH_UNCOMPARED;
519 
520   /*
521     If a condition is e.g.  prio > 4,
522     - the "4" is part of the filter-rule (rule-item "ri")
523     - the "prio" will be substitute with that field's value
524       in the log-event (the corresponding log-item, "li")
525 
526     I.e. we're lucky in that as mnemonics go, the "l" in
527     "lc", "lf", "li" can stand for both "log-event" and
528     "left in the comparison", and as can the "r" for both
529     "rule" and "right in the comparison."
530   */
531 
532   DBUG_ASSERT(ri != nullptr);
533 
534   /*
535     If there is no match, the only valid scenarios are "success"
536     (if we tested for absence), or failure (otherwise).  Handle
537     them here to make any derefs of li beyond this point safe.
538   */
539   if (li == nullptr)
540     return (ri->cond == LOG_FILTER_COND_ABSENT) ? LOG_FILTER_MATCH_SUCCESS
541                                                 : LOG_FILTER_MATCH_UNSATISFIED;
542 
543   else if (ri->cond == LOG_FILTER_COND_PRESENT)
544     return LOG_FILTER_MATCH_SUCCESS;
545 
546   else if (ri->cond == LOG_FILTER_COND_ABSENT)
547     return LOG_FILTER_MATCH_UNSATISFIED;
548 
549   // item class on left hand side / right hand side
550   rc = log_item_string_class(ri->match.item_class);
551   lc = log_item_string_class(li->item_class);
552 
553   // if one's a string and the other isn't, fail for now
554   if (rc != lc)
555     e = LOG_FILTER_MATCH_CLASSES_DIFFER;
556 
557   else {
558     e = LOG_FILTER_MATCH_UNSATISFIED;
559     double rf, lf;
560 
561     // we're comparing two strings
562     if (rc) {
563       /*
564         We're setting up lf to be the result of our string-comparison
565         (<0, 0, >0), and rf to be 0.  This allows us to use all the
566         numerical comparators below that compare lf and rf.
567       */
568 
569       rf = 0;
570 
571       // some ado in case the strings aren't \0 terminated
572       size_t len;  // length of the shorter string
573       len = (ri->match.data.data_string.length < li->data.data_string.length)
574                 ? ri->match.data.data_string.length
575                 : li->data.data_string.length;
576 
577       // compare for the shorter of the given lengths
578       lf = log_string_compare(li->data.data_string.str,
579                               ri->match.data.data_string.str, len,
580                               false);  // case-sensitive
581 
582       /*
583         If strings are the same for the shared length,
584         but are of different length, longer string "wins"
585       */
586       if ((lf == 0) &&
587           (ri->match.data.data_string.length != li->data.data_string.length))
588         lf = (li->data.data_string.length > len) ? 1 : -1;
589     }
590 
591     // we're comparing numerically
592     else {
593       // get values as floats (works for integer items as well)
594       log_item_get_float(&ri->match, &rf);
595       log_item_get_float(li, &lf);
596     }
597 
598     // do the actual comparison
599     switch (ri->cond) {
600       case LOG_FILTER_COND_EQ:  // ==  Equal
601         if (lf == rf) e = LOG_FILTER_MATCH_SUCCESS;
602         break;
603 
604       case LOG_FILTER_COND_NE:  // !=  Not Equal
605         if (lf != rf) e = LOG_FILTER_MATCH_SUCCESS;
606         break;
607 
608       case LOG_FILTER_COND_GE:  // >=  Greater or Equal
609         if (lf >= rf) e = LOG_FILTER_MATCH_SUCCESS;
610         break;
611 
612       case LOG_FILTER_COND_LT:  // <   Less Than
613         if (lf < rf) e = LOG_FILTER_MATCH_SUCCESS;
614         break;
615 
616       case LOG_FILTER_COND_LE:  // <=  Less or Equal
617         if (lf <= rf) e = LOG_FILTER_MATCH_SUCCESS;
618         break;
619 
620       case LOG_FILTER_COND_GT:  // >   Greater than
621         if (lf > rf) e = LOG_FILTER_MATCH_SUCCESS;
622         break;
623 
624         // unknown comparison type
625       default:
626         e = LOG_FILTER_MATCH_COMPARATOR_UNKNOWN; /* purecov: inspected */
627         DBUG_ASSERT(false);
628     }  // comparator switch
629   }    // class mismatch?
630 
631   return e;
632 }
633 
634 /**
635   Apply all matching rules from a filter rule set to a given log line.
636 
637   @param           ruleset              rule-set to apply
638   @param           ll                   the current log line
639 
640   @retval          int                  number of matched rules
641 */
log_builtins_filter_run(log_filter_ruleset * ruleset,log_line * ll)642 int log_builtins_filter_run(log_filter_ruleset *ruleset, log_line *ll) {
643   size_t rn;           // rule     number
644   int ln = -1;         // log-item number
645   log_filter_rule *r;  // current  rule
646   int processed = 0;
647   log_filter_match cond_result;
648 
649   DBUG_ASSERT(filter_inited);
650 
651   if (ruleset == nullptr) return 0; /* purecov: inspected */
652 
653   mysql_rwlock_rdlock(&ruleset->ruleset_lock);
654 
655   for (rn = 0; ((rn < ruleset->count) && (ll->seen != LOG_ITEM_END)); rn++) {
656     r = &ruleset->rule[rn];
657 
658     /*
659       If the rule is temporarily disabled, skip over it.
660       If and when LOG_FILTER_CHAIN_AND/LOG_FILTER_CHAIN_OR
661       are added, those chained conditions must be muted/unmuted
662       along with the first one, i.e. as a group.
663     */
664     if (r->flags & LOG_FILTER_FLAG_DISABLED) continue;
665 
666     /*
667       Look for a matching field in the event!
668     */
669 
670     /*
671       Currently applies to 0 or 1 match, there is no multi-match.
672     */
673 
674     if (r->cond == LOG_FILTER_COND_NONE)  // in ELSE etc. there is no condition
675     {
676       /*
677         We could just initialize this at a top and then chain this
678         (i.e. rules with conditions will overwrite this; while rules
679         without (i.e. ELSE) will "inherit" the previous value, but we
680         don't currently have to as implicit-DELETE-field is set up at
681         parse-time, not at execution time. For that reason, we always
682         reset this for the time being so people will find the code
683         easier to understand.
684       */
685       ln = -1;
686       goto apply_action;
687     }
688 
689     ln = log_line_index_by_item(ll, &r->match);
690 
691     /*
692       If we found a suitable field, see whether its value satisfies
693       the condition given in the rule.  If so, apply the action.
694 
695       ln == -1  would indicate "not found", which we can actually
696       match against for cases like, "if one doesn't exist, create one now."
697     */
698     cond_result = log_filter_try_match((ln >= 0) ? &ll->item[ln] : nullptr, r);
699 
700     if (cond_result == LOG_FILTER_MATCH_SUCCESS) {
701       /*
702         Protect match_count in case of non-atomic updates.
703       */
704       mysql_rwlock_wrlock(&(r->rule_lock));
705       ++r->match_count;
706       mysql_rwlock_unlock(&(r->rule_lock));
707 
708       if (r->verb == LOG_FILTER_CHAIN_AND)  // AND -- test next condition
709         continue;                           // proceed with next cond
710 
711       else if (r->verb == LOG_FILTER_CHAIN_OR)  // OR -- one match is enough
712       {  // skip any other conditions in OR
713         while (ruleset->rule[rn].verb == LOG_FILTER_CHAIN_OR) rn++;
714         r = &ruleset->rule[rn];
715       }
716       /*
717         If we're here, we either had a single-condition IF match,
718         or one condition in an OR-chain matched.
719         In either case, it's time to apply the verb now.
720       */
721     } else if (cond_result == LOG_FILTER_MATCH_UNSATISFIED) {
722       if (r->verb ==
723           LOG_FILTER_CHAIN_AND) {  // jump to next branch (ELSEIF/ELSE/ENDIF)
724         while (ruleset->rule[++rn].verb == LOG_FILTER_CHAIN_AND)
725           ;  // skip over all AND conditions
726       }
727       /*
728         skip over rule:
729         - if this was an AND-chain,  it'll skip over last cond and the action
730         - if this was an OR-chain,   it'll proceed to the next condition
731         - if this was a solitary IF, it'll skip the action
732       */
733       continue;  // skip over last condition (the one with the actual action)
734     } else       // misc. failures (type mismatch etc.)
735       continue;
736 
737   apply_action:
738     if (log_filter_try_apply(ll, ln, r) == LOG_FILTER_APPLY_SUCCESS) {
739       processed++;
740 
741       if (r->verb == LOG_FILTER_RETURN) goto done;
742     }
743 
744     if (r->jump != 0)  // we're at the end of a block; jump to ENDIF
745       rn += r->jump - 1;
746   }
747 
748 done:
749   mysql_rwlock_unlock(&ruleset->ruleset_lock);
750 
751   return processed;
752 }
753 
754 /**
755   This is part of the 5.7 emulation:
756   If --log_error_verbosity is changed, we generate an
757   artificial filter rule from it here.
758 
759   For this filtering to be active, @@global.log_error_services
760   has to feature "log_filter_internal", as it does by default.
761   When that is the case, one or both of log_error_verbosity and
762   log_error_suppression_list (see below) may be used.
763   Only one of "log_filter_internal" and "log_filter_dragnet"
764   should be used at a time.
765 
766   @param verbosity  log_error_verbosity style, range(1,3)
767                     1:errors,   2:+=warnings,  3:+=notes
768 
769   @retval            0: success
770   @retval           !0: failure
771 */
log_builtins_filter_update_verbosity(int verbosity)772 int log_builtins_filter_update_verbosity(int verbosity) {
773   size_t rn;
774   log_filter_rule *r;
775   int rr = -99;
776 
777   if (log_builtins_filter_ruleset_lock(log_filter_builtin_rules,
778                                        LOG_BUILTINS_LOCK_EXCLUSIVE) < 0)
779     return -1;
780 
781   /*
782     If a log_error_verbosity item already exists, update it.
783     This should always be the case now since we create an item on start-up
784     and different filter components can keep their rule-sets separate.
785   */
786   for (rn = 0; (rn < log_filter_builtin_rules->count); rn++) {
787     r = &log_filter_builtin_rules->rule[rn];
788 
789     if ((r->match.type == LOG_ITEM_LOG_PRIO) && (r->verb == LOG_FILTER_DROP) &&
790         (r->cond == LOG_FILTER_COND_GT)) {
791       r->match.data.data_integer = verbosity;
792       r->flags &= ~LOG_FILTER_FLAG_DISABLED;
793       rr = 0;
794       goto done;
795     }
796   }
797 
798   /* purecov: begin deadcode */
799 
800   // if no log_error_verbosity item already exists, create one
801   if (log_filter_ruleset_full(
802           log_filter_builtin_rules)) { /* since this is a private rule-set, this
803                                           should never happen */
804     rr = -2;
805     goto done;
806   }
807 
808   r = log_builtins_filter_rule_init(log_filter_builtin_rules);
809 
810   log_item_set_with_key(&r->match, LOG_ITEM_LOG_PRIO, nullptr,
811                         LOG_ITEM_FREE_NONE)
812       ->data_integer = verbosity;
813   r->cond = LOG_FILTER_COND_GT;
814   r->verb = LOG_FILTER_DROP;
815 
816   log_filter_builtin_rules->count++;
817 
818   rr = 1; /* purecov: end */
819 
820 done:
821   log_builtins_filter_ruleset_unlock(log_filter_builtin_rules);
822 
823   return rr;
824 }
825 
826 /**
827   @@global.log_error_suppression_list accepts a comma-separated
828   list of error-codes that should not be included in the error-log.
829   Events with a severity of System or Error can not be filtered
830   in this way and will always be forwarded to the log-sinks.
831 
832   This provides simple filtering for cases where the flexibility
833   of the loadable filter-language is not needed. (The same engine
834   is used however, just with a more limited interface.)
835 
836   For this filtering to be active, @@global.log_error_services has
837   to feature "log_filter_internal", as it does by default. When that
838   is the case, one or both of log_error_verbosity and this variable
839   may be used. Only one of "log_filter_internal" and "log_filter_dragnet"
840   should be used at a time.
841 
842   The semantics follow that of our system variables; that is to say,
843   when called with update==false, the function acts as a check-function
844   that validates the entire list given to it; when called with
845   update==true, it creates filter-rules from the list items. This way,
846   we either create all rules, or no rules, rather than ending up with
847   an incomplete rule-set when we encounter a problem in the input.
848 
849   The return value encodes the location in the argument where the
850   failure occurred, like so:
851   - if 0 is returned, no issues were detected
852   - if a value less than zero is returned, -(retval + 1) is the
853     byte position (counting from 0) in the list argument at
854     which point the failure was detected
855 
856   @param   list       list of error-codes that should not appear
857                       in the error-log
858   @param   update     false: verify list only
859                       true:  create filtering rules from suppression list
860 
861   @retval              0: success
862   @retval             !0: failure (see above)
863 */
log_builtins_filter_parse_suppression_list(char * list,bool update)864 int log_builtins_filter_parse_suppression_list(char *list, bool update) {
865   char symbol[80];      // error to suppress ("1234" or "MY-001234")
866   char *start = list;   // start of token
867   char *end = nullptr;  // end of token
868   size_t len;           // length of token
869   int errcode;          // error-code of token
870   int commas = -1;      // reject two separators in a row
871   int rr = 0;           // return value
872   size_t rn;            // rule number
873   log_filter_rule *r;   // rule
874   uint list_len = 0;    // number of error-codes in input
875 
876   // in update-mode, discard old suppress rules for filter rule-set
877   if (update) {
878     // lock rule-set
879     if (log_builtins_filter_ruleset_lock(log_filter_builtin_rules,
880                                          LOG_BUILTINS_LOCK_EXCLUSIVE) < 0)
881       return -1;  // bail directly (without trying to unlock)
882 
883     for (rn = 0; (rn < log_filter_builtin_rules->count);) {
884       r = &log_filter_builtin_rules->rule[rn];
885 
886       if ((r->match.type == LOG_ITEM_SQL_ERRCODE) &&
887           (r->verb == LOG_FILTER_DROP) &&
888           (r->cond == LOG_FILTER_COND_EQ))  // match "drop by errcode"
889         log_builtins_filter_rule_remove(log_filter_builtin_rules, rn);
890       else
891         rn++;
892     }
893   }
894 
895   if (list == nullptr) goto success;
896 
897   while (true) {
898     // find start of token
899     while ((*start != '\0') && (isspace(*start) || (*start == ','))) {
900       if (*start == ',') {
901         /*
902           commas at this point:
903           -1  We're looking for the first element. No comma allowed yet!
904            0  We're looking for element 2+, no comma yet, one is allowed.
905            1  We're looking for element 2+ and already have a comma!
906         */
907         if (commas != 0) goto fail;
908         commas++;
909       }
910       start++;
911     }
912     end = start;
913 
914     // find first non-token character (i.e. first character after token)
915     while ((*end != '\0') && !(isspace(*end) || (*end == ','))) end++;
916 
917     // no token found, end loop
918     if ((len = (end - start)) == 0) {
919       if (commas < 1)  // list may not end in a comma
920         goto success;
921       goto fail;
922     }
923 
924     if (commas == 0)  // element needs to be first, or follow a comma
925       goto fail;
926     else
927       commas = 0;
928 
929     // token too long, error
930     if (len >= sizeof(symbol) - 1) goto fail;
931     strncpy(symbol, start, len);
932     symbol[len] = '\0';
933 
934     // numeric token "1234"
935     if (isdigit(symbol[0])) {
936       char *last;
937       errcode = (int)strtol(symbol, &last, 10);
938       if (*last != '\0') errcode = -1;
939     }
940     // alphanumeric token ("MY-1234" or "ER_FOO")
941     else
942       errcode = mysql_symbol_to_errno(symbol);
943 
944     if (!(((errcode >= EE_ERROR_FIRST) && (errcode <= EE_ERROR_LAST)) ||
945           ((errcode >= ER_SERVER_RANGE_START) &&
946            (mysql_errno_to_builtin(errcode) >= 0))))
947       goto fail;
948 
949     if (update) {
950       // make sure we have the space
951       if (log_filter_ruleset_full(log_filter_builtin_rules)) goto fail;
952 
953       /*
954         This would require the mutex-init to fail,
955         which we can't know beforehand.
956       */
957       if ((r = log_builtins_filter_rule_init(log_filter_builtin_rules)) ==
958           nullptr)
959         goto fail; /* purecov: inspected */
960 
961       log_item_set_with_key(&r->match, LOG_ITEM_SQL_ERRCODE, nullptr,
962                             LOG_ITEM_FREE_NONE)
963           ->data_integer = errcode;
964       r->cond = LOG_FILTER_COND_EQ;
965       r->verb = LOG_FILTER_DROP;
966 
967       log_filter_builtin_rules->count++;
968     }
969 
970     /*
971       During check-phase, make sure the requested number of error-codes
972       (and therefore, the requested number of DROP rules) will fit into
973       the built-in rule-set.  Reserve one rule for --log-error-verbosity
974       and one for our "ERROR and SYSTEM always pass" shortcut.
975       Without this check, we'd still catch the (attempted) overflow during
976       assignment, but if we do it during the check phase, we protect the
977       integrity of both the current rule-set and the variable's value.
978     */
979     else if (++list_len >= (log_filter_builtin_rules->alloc - 2))
980       goto fail;
981 
982     start = end;
983   }
984 
985 fail:
986   rr = -(start - list + 1);
987 success:
988   if (update) log_builtins_filter_ruleset_unlock(log_filter_builtin_rules);
989 
990   return rr;
991 }
992 
993 /*
994   Service: built-in filter
995 */
996 
997 DEFINE_METHOD(log_filter_ruleset *, log_builtins_filter_imp::filter_ruleset_new,
998               (log_filter_tag * tag, size_t count)) {
999   return log_builtins_filter_ruleset_new(tag, count);
1000 }
1001 
1002 DEFINE_METHOD(int, log_builtins_filter_imp::filter_ruleset_lock,
1003               (log_filter_ruleset * ruleset,
1004                log_builtins_filter_lock locktype)) {
1005   return log_builtins_filter_ruleset_lock(ruleset, locktype);
1006 }
1007 
1008 DEFINE_METHOD(void, log_builtins_filter_imp::filter_ruleset_unlock,
1009               (log_filter_ruleset * ruleset)) {
1010   log_builtins_filter_ruleset_unlock(ruleset);
1011 }
1012 
1013 DEFINE_METHOD(void, log_builtins_filter_imp::filter_ruleset_drop,
1014               (log_filter_ruleset * ruleset)) {
1015   log_builtins_filter_ruleset_drop(ruleset);
1016 }
1017 
1018 DEFINE_METHOD(void, log_builtins_filter_imp::filter_ruleset_free,
1019               (log_filter_ruleset * *ruleset)) {
1020   log_builtins_filter_ruleset_free(ruleset);
1021 }
1022 
1023 DEFINE_METHOD(int, log_builtins_filter_imp::filter_ruleset_move,
1024               (log_filter_ruleset * from, log_filter_ruleset *to)) {
1025   uint32 rule_index;
1026 
1027   if (from->count > to->alloc)  // do we have enough space in target?
1028     return -1;                  /* purecov: inspected */
1029 
1030   log_builtins_filter_ruleset_drop(to);  // clear destination
1031 
1032   to->tag = from->tag;
1033 
1034   for (rule_index = 0; rule_index < from->count; rule_index++) {
1035     to->rule[rule_index] = from->rule[rule_index];
1036     memset(&from->rule[rule_index], 0, sizeof(log_filter_rule));
1037   }
1038 
1039   to->count = from->count;
1040   from->count = 0;
1041 
1042   return 0;
1043 }
1044 
1045 DEFINE_METHOD(void *, log_builtins_filter_imp::filter_rule_init,
1046               (log_filter_ruleset * ruleset)) {
1047   if (log_filter_ruleset_full(ruleset)) return nullptr;
1048   return (void *)log_builtins_filter_rule_init(ruleset);
1049 }
1050 
1051 DEFINE_METHOD(int, log_builtins_filter_imp::filter_run,
1052               (log_filter_ruleset * ruleset, log_line *ll)) {
1053   return log_builtins_filter_run(ruleset, ll);
1054 }
1055 
1056 DEFINE_METHOD(log_filter_ruleset *,
1057               log_builtins_filter_debug_imp::filter_debug_ruleset_get, (void)) {
1058   return log_filter_builtin_rules;
1059 }
1060