Coverage Report

Created: 2026-04-12 06:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/freeradius-server/src/lib/server/trigger.c
Line
Count
Source
1
/*
2
 *   This program is free software; you can redistribute it and/or modify
3
 *   it under the terms of the GNU General Public License as published by
4
 *   the Free Software Foundation; either version 2 of the License, or
5
 *   (at your option) any later version.
6
 *
7
 *   This program is distributed in the hope that it will be useful,
8
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10
 *   GNU General Public License for more details.
11
 *
12
 *   You should have received a copy of the GNU General Public License
13
 *   along with this program; if not, write to the Free Software
14
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15
 */
16
17
/*
18
 * $Id: 0f90fb463f65684425384f7a803ba6d919276c3b $
19
 *
20
 * @file trigger.c
21
 * @brief Execute scripts when a server event occurs.
22
 *
23
 * @copyright 2015 The FreeRADIUS server project
24
 */
25
RCSID("$Id: 0f90fb463f65684425384f7a803ba6d919276c3b $")
26
27
#include <freeradius-devel/protocol/freeradius/freeradius.internal.h>
28
#include <freeradius-devel/server/cf_file.h>
29
#include <freeradius-devel/server/cf_parse.h>
30
#include <freeradius-devel/server/exec.h>
31
#include <freeradius-devel/server/main_loop.h>
32
#include <freeradius-devel/server/map.h>
33
#include <freeradius-devel/server/pair.h>
34
#include <freeradius-devel/server/request_data.h>
35
#include <freeradius-devel/server/trigger.h>
36
#include <freeradius-devel/unlang/function.h>
37
#include <freeradius-devel/unlang/subrequest.h>
38
#include <freeradius-devel/unlang/tmpl.h>
39
40
41
#include <sys/wait.h>
42
43
/** Whether triggers are enabled globally
44
 *
45
 */
46
static CONF_SECTION const *trigger_cs;
47
static fr_rb_tree_t   *trigger_last_fired_tree;
48
static pthread_mutex_t    *trigger_mutex;
49
50
/** Describes a rate limiting entry for a trigger
51
 *
52
 */
53
typedef struct {
54
  fr_rb_node_t  node;   //!< Entry in the trigger last fired tree.
55
  CONF_ITEM *ci;    //!< Config item this rate limit counter is associated with.
56
  fr_time_t last_fired; //!< When this trigger last fired.
57
} trigger_last_fired_t;
58
59
static fr_dict_t const *dict_freeradius;
60
extern fr_dict_autoload_t trigger_dict[];
61
fr_dict_autoload_t trigger_dict[] = {
62
  { .out = &dict_freeradius, .proto = "freeradius" },
63
  DICT_AUTOLOAD_TERMINATOR
64
};
65
66
static fr_dict_attr_t const *attr_trigger_name;
67
extern fr_dict_attr_autoload_t trigger_dict_attr[];
68
fr_dict_attr_autoload_t trigger_dict_attr[] = {
69
  { .out = &attr_trigger_name, .name = "Trigger-Name", .type = FR_TYPE_STRING, .dict = &dict_freeradius },
70
  DICT_AUTOLOAD_TERMINATOR
71
};
72
73
static void _trigger_last_fired_free(void *data)
74
0
{
75
0
  talloc_free(data);
76
0
}
77
78
/** Compares two last fired structures
79
 *
80
 * @param one first pointer to compare.
81
 * @param two second pointer to compare.
82
 * @return CMP(one, two)
83
 */
84
static int8_t _trigger_last_fired_cmp(void const *one, void const *two)
85
0
{
86
0
  trigger_last_fired_t const *a = one, *b = two;
87
88
0
  return CMP(a->ci, b->ci);
89
0
}
90
91
/** Return whether triggers are enabled
92
 *
93
 */
94
bool trigger_enabled(void)
95
0
{
96
0
  return (trigger_cs != NULL);
97
0
}
98
99
typedef struct {
100
  fr_value_box_list_t out;    //!< result of the xlap (which we ignore)
101
  unlang_result_t   result;   //!< the result of expansion
102
  tmpl_t      *vpt;   //!< the template to execute
103
  int     exec_status;  //!< Result of the program (if the trigger is a tmpl)
104
} fr_trigger_t;
105
106
/** Execute a trigger - call an executable to process an event
107
 *
108
 * A trigger ties a state change (e.g. connection up) in a module to an action
109
 * (e.g. send an SNMP trap) defined in triggers.conf or in the trigger
110
 * section of a module.  There's no setup for triggers, the triggering code
111
 * just calls this function with the name of the trigger to run, and an optional
112
 * interpreter if the trigger should run asynchronously.
113
 *
114
 * If no interpreter is passed in, the trigger runs synchronously, which is
115
 * useful when the server is shutting down and we want to ensure that the
116
 * trigger has completed before the server exits.
117
 *
118
 * If an interpreter is passed in, the trigger runs asynchronously in that
119
 * interpreter, allowing the server to continue processing packets while the
120
 * trigger runs.
121
 *
122
 * The name of each trigger is based on the module or portion of the server
123
 * which runs the trigger, and is usually taken from the state when the module
124
 * has a state change.
125
 *
126
 * Triggers are separate from logs, because log messages are generally
127
 * informational, are not time sensitive, and usually require log files to be
128
 * parsed and filtered in order to find relevant information.
129
 *
130
 * In contrast, triggers are something specific which the administrator needs
131
 * to be notified about immediately and can't wait to post-process a log file.
132
 *
133
 * @note Calls to this function will be ignored if #trigger_init has not been called.
134
 *
135
 * @param[in] intp    Interpreter to run the trigger with.  If this is NULL the
136
 *        trigger will be executed synchronously.
137
 * @param[in] cs    to search for triggers in.
138
 *        If cs is not NULL, the portion after the last '.' in name is used for the trigger.
139
 *        If cs is NULL, the entire name is used to find the trigger in the global trigger
140
 *        section.
141
 * @param[in,out] trigger_cp  Optional pointer to a CONF_PAIR pointer.  If populated and the pointer is not
142
 *        NULL, this CONF_PAIR will be used rather than searching.
143
 *        If populated, and the pointer is NULL, the search will happen and the pointer
144
 *        will be populated with the found CONF_PAIR.
145
 * @param[in] name    the path relative to the global trigger section ending in the trigger name
146
 *        e.g. module.ldap.pool.start.
147
 * @param[in] rate_limit  whether to rate limit triggers.
148
 * @param[in] args    to populate the trigger's request list with.
149
 * @return
150
 *  - 0 on success.
151
 *  - -1 if the trigger is not defined.
152
 *  - -2 if the trigger was rate limited.
153
 *  - -3 on failure.
154
 */
155
int trigger(unlang_interpret_t *intp, CONF_SECTION const *cs, CONF_PAIR **trigger_cp,
156
      char const *name, bool rate_limit, fr_pair_list_t *args)
157
0
{
158
0
  CONF_ITEM   *ci;
159
0
  CONF_PAIR   *cp;
160
161
0
  char const    *attr;
162
0
  char const    *value;
163
164
0
  request_t   *request;
165
0
  fr_trigger_t    *trigger;
166
0
  ssize_t     slen;
167
168
0
  fr_event_list_t   *el;
169
0
  tmpl_rules_t    t_rules;
170
171
  /*
172
   *  noop if trigger_init was never called, or if
173
   *  we're just checking the configuration.
174
   */
175
0
  if (!trigger_cs || check_config) return 0;
176
177
  /*
178
   *  Do we have a cached conf pair?
179
   */
180
0
  if (trigger_cp && *trigger_cp) {
181
0
    cp = *trigger_cp;
182
0
    ci = cf_pair_to_item(cp);
183
0
    goto cp_found;
184
0
  }
185
186
  /*
187
   *  A module can have a local "trigger" section.  In which
188
   *  case that is used in preference to the global one.
189
   *
190
   *  @todo - we should really allow triggers via @trigger,
191
   *  so that all of the triggers are in one location.  And
192
   *  then we can have different triggers for different
193
   *  module instances.
194
   */
195
0
  if (cs) {
196
0
    cs = cf_section_find(cs, "trigger", NULL);
197
0
    if (!cs) goto use_global;
198
199
    /*
200
     *  If a local trigger{...} section exists, then
201
     *  use the local part of the name, rather than
202
     *  the full path.
203
     */
204
0
    attr = strrchr(name, '.');
205
0
    if (attr) {
206
0
      attr++;
207
0
    } else {
208
0
      attr = name;
209
0
    }
210
0
  } else {
211
0
  use_global:
212
0
    cs = trigger_cs;
213
0
    attr = name;
214
0
  }
215
216
  /*
217
   *  Find the trigger.  Note that we do NOT allow searching
218
   *  from the root of the tree.  Triggers MUST be in a
219
   *  trigger{...} section.
220
   */
221
0
  ci = cf_reference_item(cs, cs, attr);
222
0
  if (!ci) {
223
0
    if (cs != trigger_cs) goto use_global; /* not found locally, try to find globally */
224
225
0
    DEBUG3("Failed finding trigger '%s'", attr);
226
0
    return -1;
227
0
  }
228
229
0
  if (!cf_item_is_pair(ci)) {
230
0
    ERROR("Trigger is not a configuration variable: %s", attr);
231
0
    return -1;
232
0
  }
233
234
0
  cp = cf_item_to_pair(ci);
235
0
  if (!cp) return -1;
236
237
0
  if (trigger_cp) *trigger_cp = cp;
238
239
0
cp_found:
240
0
  value = cf_pair_value(cp);
241
0
  if (!value) {
242
0
    DEBUG3("Trigger has no value: %s", name);
243
0
    return -1;
244
0
  }
245
246
  /*
247
   *  Perform periodic rate_limiting.
248
   */
249
0
  if (rate_limit) {
250
0
    trigger_last_fired_t  find, *found;
251
0
    fr_time_t   now = fr_time();
252
253
0
    find.ci = ci;
254
255
0
    pthread_mutex_lock(trigger_mutex);
256
257
0
    found = fr_rb_find(trigger_last_fired_tree, &find);
258
0
    if (!found) {
259
0
      MEM(found = talloc(NULL, trigger_last_fired_t));
260
0
      found->ci = ci;
261
      /*
262
       *  Initialise last_fired to 2 seconds ago so
263
       *  the trigger fires on the first occurrence
264
       */
265
0
      found->last_fired = fr_time_wrap(NSEC * -2);
266
267
0
      fr_rb_insert(trigger_last_fired_tree, found);
268
0
    }
269
270
    /*
271
     *  Send the rate_limited traps at most once per second.
272
     *
273
     *  @todo - make this configurable for longer periods of time.
274
     */
275
0
    if (fr_time_to_sec(found->last_fired) == fr_time_to_sec(now)) {
276
0
      pthread_mutex_unlock(trigger_mutex);
277
0
      return -2;
278
0
    }
279
280
0
    found->last_fired = now;
281
0
    pthread_mutex_unlock(trigger_mutex);
282
0
  }
283
284
  /*
285
   *  Allocate a request to run asynchronously in the interpreter.
286
   */
287
0
  request = request_local_alloc_internal(NULL, (&(request_init_args_t){ .detachable = true }));
288
0
  request->name = talloc_typed_asprintf(request, "trigger-%s", name);
289
290
0
  if (args) {
291
0
    fr_pair_t *vp;
292
293
0
    if (fr_pair_list_copy(request->request_ctx, &request->request_pairs, args) < 0) {
294
0
      PERROR("Failed copying trigger arguments");
295
296
0
    fail:
297
0
      talloc_free(request);
298
0
      return -3;
299
0
    }
300
301
    /*
302
     *  Add the trigger name to the request data
303
     */
304
0
    MEM(pair_append_request(&vp, attr_trigger_name) >= 0);
305
0
    fr_pair_value_strdup(vp, cf_pair_attr(cp), false);
306
0
  }
307
308
0
  MEM(trigger = talloc_zero(request, fr_trigger_t));
309
0
  fr_value_box_list_init(&trigger->out);
310
311
0
  el = unlang_interpret_event_list(request);
312
0
  if (!el) el = main_loop_event_list();
313
314
  /*
315
   *  During shutdown there may be no event list, so nothing much can be done.
316
   */
317
0
  if (unlikely(!el)) goto fail;
318
319
320
0
  t_rules = (tmpl_rules_t) {
321
0
    .attr = {
322
0
      .dict_def = request->local_dict, /* we can use local attributes */
323
0
      .list_def = request_attr_request,
324
0
    },
325
0
    .xlat = {
326
0
      .runtime_el = el,
327
0
    },
328
0
    .at_runtime = true,
329
0
  };
330
331
0
  slen = tmpl_afrom_substr(trigger, &trigger->vpt, &FR_SBUFF_IN(value, talloc_strlen(value)),
332
0
         cf_pair_value_quote(cp), NULL, &t_rules);
333
0
  if (slen <= 0) {
334
0
    char *spaces, *text;
335
336
0
    fr_canonicalize_error(trigger, &spaces, &text, slen, value);
337
338
0
    cf_log_err(cp, "Failed parsing trigger expression");
339
0
    cf_log_err(cp, "%s", text);
340
0
    cf_log_perr(cp, "%s^", spaces);
341
342
0
    goto fail;
343
0
  }
344
345
0
  if (!tmpl_is_exec(trigger->vpt) && !tmpl_is_xlat(trigger->vpt)) {
346
    /*
347
     *  We only support exec and xlat templates.
348
     *  Anything else is an error.
349
     */
350
0
    cf_log_err(cp, "Trigger must be an \"expr\" or `exec`");
351
0
    goto fail;
352
0
  }
353
354
0
  fr_assert(trigger->vpt != NULL);
355
356
0
  if (unlang_tmpl_push(trigger, &trigger->result, &trigger->out, request, trigger->vpt,
357
0
           &(unlang_tmpl_args_t) {
358
0
        .type = UNLANG_TMPL_ARGS_TYPE_EXEC,
359
0
        .exec = {
360
0
          .status_out = &trigger->exec_status,
361
0
          .timeout = fr_time_delta_from_sec(5),
362
0
          },
363
0
           }, UNLANG_TOP_FRAME) < 0) {
364
0
    goto fail;
365
0
  }
366
367
  /*
368
   *  An interpreter was passed in, we can run the expansion
369
   *  asynchronously in that interpreter.  And then the
370
   *  worker cleans up the detached request.
371
   */
372
0
  if (intp) {
373
0
    unlang_interpret_set(request, intp);
374
375
    /*
376
     *  Don't allow the expansion to run for a long time.
377
     *
378
     *  @todo - make the timeout configurable.
379
     */
380
0
    if (unlang_interpret_set_timeout(request, fr_time_delta_from_sec(1)) < 0) {
381
0
      DEBUG("Failed setting timeout on trigger %s", value);
382
0
      goto fail;
383
0
    }
384
385
0
    if (unlang_subrequest_child_push_and_detach(request) < 0) {
386
0
      PERROR("Running trigger failed");
387
0
      goto fail;
388
0
    }
389
0
  } else {
390
    /*
391
     *  No interpreter, we MUST be running from the
392
     *  main loop.  We then run the expansion
393
     *  synchronously.  This allows the expansion /
394
     *  notification to finish before the server shuts
395
     *  down.
396
     *
397
     *  If the expansion was async, then it may be
398
     *  possible for the server to exit before the
399
     *  expansion finishes.  Arguably the worker
400
     *  thread should ensure that the server doesn't
401
     *  exit until all requests have acknowledged that
402
     *  they've exited.
403
     *
404
     *  But those exits may be advisory.  i.e. "please
405
     *  finish the request".  This one here is
406
     *  mandatary to finish before the server exits.
407
     */
408
0
    unlang_interpret_synchronous(NULL, request);
409
0
    talloc_free(request);
410
0
  }
411
412
0
  return 0;
413
0
}
414
415
/** Create trigger arguments to describe the server the pool connects to
416
 *
417
 * @note #trigger_init must be called before calling this function,
418
 *   else it will return NULL.
419
 *
420
 * @param[in] ctx to allocate fr_pair_t s in.
421
 * @param[out] list to append Pool-Server and Pool-Port pairs to
422
 * @param[in] server  we're connecting to.
423
 * @param[in] port  on that server.
424
 */
425
void trigger_args_afrom_server(TALLOC_CTX *ctx, fr_pair_list_t *list, char const *server, uint16_t port)
426
{
427
  fr_dict_attr_t const  *server_da;
428
  fr_dict_attr_t const  *port_da;
429
  fr_pair_t   *vp;
430
431
  server_da = fr_dict_attr_child_by_num(fr_dict_root(fr_dict_internal()), FR_CONNECTION_POOL_SERVER);
432
  if (!server_da) {
433
    ERROR("Incomplete dictionary: Missing definition for \"Connection-Pool-Server\"");
434
    return;
435
  }
436
437
  port_da = fr_dict_attr_child_by_num(fr_dict_root(fr_dict_internal()), FR_CONNECTION_POOL_PORT);
438
  if (!port_da) {
439
    ERROR("Incomplete dictionary: Missing definition for \"Connection-Pool-Port\"");
440
    return;
441
  }
442
443
  MEM(vp = fr_pair_afrom_da(ctx, server_da));
444
  fr_pair_value_strdup(vp, server, false);
445
  fr_pair_append(list, vp);
446
447
  MEM(vp = fr_pair_afrom_da(ctx, port_da));
448
  vp->vp_uint16 = port;
449
  fr_pair_append(list, vp);
450
}
451
452
/**  Callback to verify that trigger_args map is valid
453
 */
454
static int trigger_args_validate(map_t *map, UNUSED void *uctx)
455
0
{
456
0
  if (map->lhs->type != TMPL_TYPE_ATTR) {
457
0
    cf_log_err(map->ci, "%s is not an internal attribute reference", map->lhs->name);
458
0
    return -1;
459
0
  }
460
461
0
  switch (map->rhs->type) {
462
0
  case TMPL_TYPE_DATA_UNRESOLVED:
463
0
    if (tmpl_resolve(map->rhs, NULL) < 0) {
464
0
      cf_log_err(map->ci, "Invalid data %s", map->rhs->name);
465
0
      return -1;
466
0
    }
467
0
    break;
468
0
  case TMPL_TYPE_DATA:
469
0
    break;
470
471
0
  default:
472
0
    cf_log_err(map->ci, "Right hand side of trigger_args must be literal, not %s", tmpl_type_to_str(map->rhs->type));
473
0
    return -1;
474
0
  }
475
476
0
  return 0;
477
0
}
478
479
0
#define BUILD_ATTR(_name, _value) if (_value) { \
480
0
  da = fr_dict_attr_by_name(NULL, fr_dict_root(fr_dict_internal()), _name); \
481
0
  if (!da) { \
482
0
    ERROR("Incomplete dictionary: Missing definition for \"" _name "\""); \
483
0
    return -1; \
484
0
  } \
485
0
  MEM(vp = fr_pair_afrom_da(ctx, da)); \
486
0
  fr_pair_value_strdup(vp, _value, false); \
487
0
  fr_pair_append(list, vp); \
488
0
}
489
490
/** Build trigger args pair list for modules
491
 *
492
 * @param[in] ctx to allocate pairs in.
493
 * @param[in] list  to populate.
494
 * @param[in] cs  CONF_SECTION to search for a "trigger_args" section
495
 * @param[in] args  Common module data which will populate default pairs
496
 */
497
int module_trigger_args_build(TALLOC_CTX *ctx, fr_pair_list_t *list, CONF_SECTION const *cs, module_trigger_args_t *args)
498
0
{
499
0
  map_list_t    *maps;
500
0
  map_t     *map = NULL;
501
0
  fr_pair_t   *vp;
502
0
  fr_dict_attr_t const  *da;
503
0
  tmpl_rules_t    t_rules = (tmpl_rules_t){
504
0
            .attr = {
505
0
              .dict_def = fr_dict_internal(),
506
0
              .list_def = request_attr_request,
507
0
            },
508
0
          };
509
510
  /*
511
   *  Build the default pairs from the module data
512
   */
513
0
  BUILD_ATTR("Module-Name", args->module)
514
0
  BUILD_ATTR("Module-Instance", args->name)
515
0
  BUILD_ATTR("Connection-Pool-Server", args->server)
516
0
  da = fr_dict_attr_child_by_num(fr_dict_root(fr_dict_internal()), FR_CONNECTION_POOL_PORT);
517
0
  if (!da) {
518
0
    ERROR("Incomplete dictionary: Missing definition for \"Connection-Pool-Port\"");
519
0
    return -1;
520
0
  }
521
0
  MEM(vp = fr_pair_afrom_da(ctx, da));
522
0
  vp->vp_uint16 = args->port;
523
0
  fr_pair_append(list, vp);
524
525
  /*
526
   *  If a CONF_SECTION has been passed in, look for a "trigger_args"
527
   *  sub section and parse that as a map to create additional pairs.
528
   */
529
0
  if (!cs) return 0;
530
0
  cs = cf_section_find(cs, "trigger_args", NULL);
531
0
  if (!cs) return 0;
532
533
0
  MEM(maps = talloc(NULL, map_list_t));
534
0
  map_list_init(maps);
535
0
  if (map_afrom_cs(maps, maps, cs, &t_rules, &t_rules, trigger_args_validate, NULL, 256) < 0) {
536
0
  fail:
537
0
    talloc_free(maps);
538
0
    return -1;
539
0
  }
540
541
0
  while ((map = map_list_next(maps, map))) {
542
0
    MEM(vp = fr_pair_afrom_da_nested(ctx, list, tmpl_attr_tail_da(map->lhs)));
543
0
    if (fr_value_box_cast(vp, &vp->data, vp->da->type, vp->da, &map->rhs->data.literal) < 0) goto fail;
544
0
  }
545
0
  talloc_free(maps);
546
0
  return 0;
547
0
}
548
549
static int _mutex_free(pthread_mutex_t *mutex)
550
0
{
551
0
  pthread_mutex_destroy(mutex);
552
0
  return 0;
553
0
}
554
555
/** Free trigger resources
556
 *
557
 */
558
static int _trigger_free(UNUSED void *uctx)
559
0
{
560
0
  fr_dict_autofree(trigger_dict);
561
0
  TALLOC_FREE(trigger_last_fired_tree);
562
0
  TALLOC_FREE(trigger_mutex);
563
564
0
  return 0;
565
0
}
566
567
/** Set the global trigger section trigger will search in, and register xlats
568
 *
569
 * This function exists because triggers are used by the connection pool, which
570
 * is used in the server library which may not have the mainconfig available.
571
 * Additionally, utilities may want to set their own root config sections.
572
 *
573
 * We don't register the trigger xlat here, as we may inadvertently initialise
574
 * the xlat code, which is annoying when this is called from a utility.
575
 *
576
 * @param[in] cs_arg  to use as global trigger section.
577
 * @return
578
 *  - 0 on success.
579
 *  - -1 on failure.
580
 */
581
static int _trigger_init(void *cs_arg)
582
0
{
583
0
  CONF_SECTION *cs;
584
585
0
  if (unlikely(fr_dict_autoload(trigger_dict) < 0)) {
586
0
    PERROR("Failed loading trigger dictionaries");
587
0
    return -1;
588
0
  }
589
0
  if (unlikely(fr_dict_attr_autoload(trigger_dict_attr) < 0)) {
590
0
    PERROR("Failed loading trigger attributes");
591
0
    return -1;
592
0
  }
593
594
0
  cs = talloc_get_type_abort(cs_arg, CONF_SECTION);
595
0
  if (!cs) {
596
0
    ERROR("%s - Pointer to main_config was NULL", __FUNCTION__);
597
0
    return -1;
598
0
  }
599
600
0
  trigger_cs = cf_section_find(cs, "trigger", NULL);
601
0
  if (!trigger_cs) {
602
0
    WARN("trigger { ... } subsection not found, triggers will be disabled");
603
0
    return 0;
604
0
  }
605
606
0
  MEM(trigger_last_fired_tree = fr_rb_inline_talloc_alloc(talloc_null_ctx(),
607
0
                trigger_last_fired_t, node,
608
0
                _trigger_last_fired_cmp, _trigger_last_fired_free));
609
610
0
  trigger_mutex = talloc(talloc_null_ctx(), pthread_mutex_t);
611
0
  if (pthread_mutex_init(trigger_mutex, 0) != 0) {
612
0
    PERROR("Failed to initialize trigger mutex");
613
0
    talloc_free(trigger_mutex);
614
0
    return -1;
615
0
  }
616
617
0
  talloc_set_destructor(trigger_mutex, _mutex_free);
618
619
0
  return 0;
620
0
}
621
622
int trigger_init(CONF_SECTION const *cs)
623
0
{
624
0
  int ret;
625
626
0
  fr_atexit_global_once_ret(&ret, _trigger_init, _trigger_free, UNCONST(CONF_SECTION *, cs));
627
628
0
  return ret;
629
0
}