Coverage Report

Created: 2026-09-28 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/freeradius-server/src/lib/unlang/tmpl.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: 7a7d7dbeaa8ec60f93ec0314bb42683fbc0c0400 $
19
 *
20
 * @file unlang/tmpl.c
21
 * @brief Defines functions for calling tmpl__t asynchronously
22
 *
23
 * @copyright 2021 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
24
 * @copyright 2020 Network RADIUS SAS (legal@networkradius.com)
25
 */
26
RCSID("$Id: 7a7d7dbeaa8ec60f93ec0314bb42683fbc0c0400 $")
27
28
#include <freeradius-devel/unlang/tmpl.h>
29
#include <freeradius-devel/server/exec.h>
30
#include <freeradius-devel/util/syserror.h>
31
#include <freeradius-devel/unlang/mod_action.h>
32
#include "tmpl_priv.h"
33
#include <signal.h>
34
35
#if defined(__linux__) || defined(__FreeBSD__)
36
#include <sys/wait.h>
37
#endif
38
39
/** Send a signal (usually stop) to a request
40
 *
41
 * This is typically called via an "async" action, i.e. an action
42
 * outside of the normal processing of the request.
43
 *
44
 * If there is no #fr_unlang_tmpl_signal_t callback defined, the action is ignored.
45
 *
46
 * @param[in] request   The current request.
47
 * @param[in] frame   being signalled.
48
 * @param[in] action    to signal.
49
 */
50
static void unlang_tmpl_signal(request_t *request, unlang_stack_frame_t *frame, fr_signal_t action)
51
0
{
52
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state,
53
0
                       unlang_frame_state_tmpl_t);
54
55
  /*
56
   *  If we're cancelled, then kill any child processes
57
   */
58
0
  if ((action == FR_SIGNAL_CANCEL) && state->exec_result.request) fr_exec_oneshot_cleanup(&state->exec_result, SIGKILL);
59
60
0
  if (!state->signal) return;
61
62
0
  state->signal(request, state->rctx, action);
63
64
  /*
65
   *  If we're cancelled then disable this signal handler.
66
   *  fr_exec_oneshot_cleanup should handle being called spuriously.
67
   */
68
0
  if (action == FR_SIGNAL_CANCEL) state->signal = NULL;
69
0
}
70
71
/** Wrapper to call a resumption function after a tmpl has been expanded
72
 *
73
 *  If the resumption function returns YIELD, then this function is
74
 *  called repeatedly until the resumption function returns a final
75
 *  value.
76
 */
77
static unlang_action_t unlang_tmpl_resume(unlang_result_t *p_result, request_t *request, unlang_stack_frame_t *frame)
78
0
{
79
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
80
0
  unlang_tmpl_t     *ut = unlang_generic_to_tmpl(frame->instruction);
81
82
0
  if (tmpl_eval_cast_in_place(&state->list, request, ut->tmpl) < 0) {
83
0
    RPEDEBUG("Failed casting expansion");
84
0
    RETURN_UNLANG_FAIL;
85
0
  }
86
87
0
  if (state->out) fr_value_box_list_move(state->out, &state->list);
88
89
0
  if (state->resume) return state->resume(p_result, request, state->rctx);
90
91
0
  RETURN_UNLANG_OK;
92
0
}
93
94
/** Wrapper to call exec after the program has finished executing
95
 *
96
 */
97
static unlang_action_t unlang_tmpl_exec_wait_final(unlang_result_t *p_result, request_t *request,
98
               unlang_stack_frame_t *frame)
99
0
{
100
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state,
101
0
                       unlang_frame_state_tmpl_t);
102
103
  /*
104
   *  The exec failed for some internal reason.  We don't
105
   *  care about output, and we don't care about the programs exit status.
106
   */
107
0
  if (state->exec_result.failed) {
108
0
    fr_value_box_list_talloc_free(&state->list);
109
0
    goto resume;
110
0
  }
111
112
0
  fr_assert(state->exec_result.pid < 0); /* Assert this has been cleaned up */
113
114
0
  if (!state->args.exec.stdout_on_error && (state->exec_result.status != 0)) {
115
0
    fr_assert(fr_value_box_list_empty(&state->list));
116
0
    goto resume;
117
0
  }
118
119
  /*
120
   *  We might want to just get the status of the program,
121
   *  and not care about the output.
122
   *
123
   *  If we do care about the output, it's unquoted, and tainted.
124
   *
125
   *  FIXME - It would be much more efficient to just reparent
126
   *  the string buffer into the context of the box... but we'd
127
   *  need to fix talloc first.
128
   */
129
0
  if (state->out) {
130
0
    fr_type_t type = FR_TYPE_STRING;
131
0
    fr_value_box_t *box;
132
133
    /*
134
     *  Remove any trailing LF / CR
135
     */
136
0
    fr_sbuff_trim(&state->exec_result.stdout_buff, sbuff_char_line_endings);
137
138
0
    fr_value_box_list_init(&state->list);
139
0
    MEM(box = fr_value_box_alloc(state->ctx, FR_TYPE_STRING, NULL));
140
0
    if (fr_value_box_from_str(state->ctx, box, type, NULL,
141
0
            fr_sbuff_start(&state->exec_result.stdout_buff),
142
0
            fr_sbuff_used(&state->exec_result.stdout_buff),
143
0
            NULL) < 0) {
144
0
      talloc_free(box);
145
0
      RETURN_UNLANG_FAIL;
146
0
    }
147
0
    fr_value_box_list_insert_head(&state->list, box);
148
0
  }
149
150
0
resume:
151
  /*
152
   *  Inform the caller of the status if it asked for it
153
   */
154
0
  if (state->args.exec.status_out) *state->args.exec.status_out = state->exec_result.status;
155
156
  /*
157
   *  Ensure that the callers resume function is called.
158
   */
159
0
  frame->process = unlang_tmpl_resume;
160
0
  return unlang_tmpl_resume(p_result, request, frame);
161
0
}
162
163
/** Wrapper to call after an xlat has been expanded
164
 *
165
 */
166
static unlang_action_t unlang_tmpl_xlat_resume(unlang_result_t *p_result, request_t *request,
167
                 unlang_stack_frame_t *frame)
168
0
{
169
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
170
171
0
  if (!XLAT_RESULT_SUCCESS(&state->xlat_result)) RETURN_UNLANG_FAIL;
172
173
  /*
174
   *  Ensure that the callers resume function is called.
175
   */
176
0
  frame->process = unlang_tmpl_resume;
177
0
  return unlang_tmpl_resume(p_result, request, frame);
178
0
}
179
180
181
/** Wrapper to call exec after a tmpl has been expanded
182
 *
183
 */
184
static unlang_action_t unlang_tmpl_exec_wait_resume(unlang_result_t *p_result, request_t *request,
185
                unlang_stack_frame_t *frame)
186
0
{
187
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
188
189
0
  if (!XLAT_RESULT_SUCCESS(&state->xlat_result)) RETURN_UNLANG_FAIL;
190
191
0
  if (fr_exec_oneshot(state->ctx, &state->exec_result, request,
192
0
        &state->list,
193
0
        state->args.exec.env, false, false,
194
0
        false,
195
0
        (state->out != NULL), state,
196
0
        state->args.exec.timeout) < 0) {
197
0
    RPEDEBUG("Failed executing program");
198
0
    RETURN_UNLANG_FAIL;
199
0
  }
200
201
0
  fr_value_box_list_talloc_free(&state->list); /* this is the xlat expansion, and not the output string we want */
202
0
  frame_repeat(frame, unlang_tmpl_exec_wait_final);
203
204
0
  return UNLANG_ACTION_YIELD;
205
0
}
206
207
208
static unlang_action_t unlang_tmpl(unlang_result_t *p_result, request_t *request, unlang_stack_frame_t *frame)
209
0
{
210
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
211
0
  unlang_tmpl_t     *ut = unlang_generic_to_tmpl(frame->instruction);
212
213
  /*
214
   *  If we're not called from unlang_tmpl_push(), then
215
   *  ensure that we clean up the resulting value boxes
216
   *  and that the list to write the boxes in is initialised.
217
   */
218
0
  if (!state->ctx) {
219
0
    state->ctx = state;
220
0
    fr_value_box_list_init(&state->list);
221
0
  }
222
223
  /*
224
   *  Synchronous tmpls can just be resolved immediately, and directly to the output list.
225
   *
226
   *  However, xlat expansions (including fully synchronous function calls!) need to be expanded by
227
   *  the xlat framework.
228
   */
229
0
  if (!tmpl_async_required(ut->tmpl) && !tmpl_contains_xlat(ut->tmpl)) {
230
0
    if (tmpl_eval(state->ctx, state->out, request, ut->tmpl) < 0) {
231
0
      RPEDEBUG("Failed evaluating expansion");
232
0
      goto fail;
233
0
    }
234
235
0
    RETURN_UNLANG_OK;
236
0
  }
237
238
  /*
239
   *  XLAT structs are allowed.
240
   */
241
0
  if (tmpl_is_xlat(ut->tmpl)) {
242
0
    frame_repeat(frame, unlang_tmpl_xlat_resume);
243
0
    goto push;
244
0
  }
245
246
0
  fr_assert(tmpl_is_exec(ut->tmpl));
247
248
  /*
249
   *  Expand the arguments to the program we're executing.
250
   */
251
0
  frame_repeat(frame, unlang_tmpl_exec_wait_resume);
252
0
push:
253
0
  if (unlang_xlat_push(state->ctx, &state->xlat_result, &state->list, request, tmpl_xlat(ut->tmpl), UNLANG_SUB_FRAME) < 0) {
254
0
  fail:
255
0
    RETURN_UNLANG_ACTION_FATAL;
256
0
  }
257
258
0
  return UNLANG_ACTION_PUSHED_CHILD;
259
0
}
260
261
/** Push a tmpl onto the stack for evaluation
262
 *
263
 * @param[in] ctx   To allocate value boxes and values in.
264
 * @param[out] p_result         The frame result
265
 * @param[out] out    The value_box created from the tmpl.  May be NULL,
266
 *        in which case the result is discarded.
267
 * @param[in] request   The current request.
268
 * @param[in] tmpl    the tmpl to expand
269
 * @param[in] args    additional controls for expanding #TMPL_TYPE_EXEC,
270
 *        and where the status of exited programs will be stored.
271
 * @param[in] top_frame   If true, then this is the top frame of the sub-stack.
272
 * @return
273
 *  - 0 on success
274
 *  - -1 on failure
275
 */
276
int unlang_tmpl_push(TALLOC_CTX *ctx, unlang_result_t *p_result, fr_value_box_list_t *out, request_t *request,
277
         tmpl_t const *tmpl, unlang_tmpl_args_t *args, bool top_frame)
278
0
{
279
0
  unlang_stack_t      *stack = request->stack;
280
0
  unlang_stack_frame_t    *frame;
281
0
  unlang_frame_state_tmpl_t *state;
282
283
0
  unlang_tmpl_t     *ut;
284
285
0
  static unlang_t const tmpl_instruction_return = {
286
0
    .type = UNLANG_TYPE_TMPL,
287
0
    .name = "tmpl",
288
0
    .debug_name = "tmpl",
289
0
    .actions = MOD_ACTIONS_FAIL_TIMEOUT_RETURN,
290
0
  };
291
292
0
  static const unlang_t tmpl_instruction_fail = {
293
0
    .type = UNLANG_TYPE_TMPL,
294
0
    .name = "tmpl",
295
0
    .debug_name = "tmpl",
296
0
    .actions = DEFAULT_MOD_ACTIONS,
297
0
  };
298
299
0
  if (tmpl_needs_resolving(tmpl)) {
300
0
    REDEBUG("Expansion \"%pV\" needs to be resolved before it is used", fr_box_strvalue_len(tmpl->name, tmpl->len));
301
0
    return -1;
302
0
  }
303
304
  /*
305
   *  Avoid an extra stack frame and more work.  But only if the caller hands us a result.
306
   *  Otherwise, we have to return UNLANG_FAIL.
307
   */
308
0
  if (p_result && (tmpl_rules_cast(tmpl) == FR_TYPE_NULL) && tmpl_is_xlat(tmpl)) {
309
0
    return unlang_xlat_push(ctx, p_result, out, request, tmpl_xlat(tmpl), UNLANG_SUB_FRAME);
310
0
  }
311
312
0
  fr_assert(!tmpl_contains_regex(tmpl));
313
314
0
  MEM(ut = talloc(stack, unlang_tmpl_t));
315
0
  *ut = (unlang_tmpl_t){
316
0
    .self =  p_result ? tmpl_instruction_fail : tmpl_instruction_return,
317
0
    .tmpl = tmpl
318
0
  };
319
0
  unlang_type_init(&ut->self, NULL, UNLANG_TYPE_TMPL);
320
321
  /*
322
   *  Push a new tmpl frame onto the stack
323
   */
324
0
  if (unlang_interpret_push(p_result, request, unlang_tmpl_to_generic(ut),
325
0
          FRAME_CONF(RLM_MODULE_NOT_SET, top_frame), UNLANG_NEXT_STOP) < 0) return -1;
326
327
0
  frame = &stack->frame[stack->depth];
328
0
  state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
329
330
  /*
331
   *  Set the frame as repeatable so that multiple tmpls can
332
   *  be pushed on the stack before returning UNLANG_ACTION_PUSHED_CHILD
333
   */
334
0
  repeatable_set(frame);
335
336
0
  *state = (unlang_frame_state_tmpl_t) {
337
0
    .vpt = tmpl,
338
0
    .out = out,
339
0
    .ctx = ctx,
340
0
  };
341
0
  if (args) state->args = *args; /* Copy these because they're usually ephemeral/initialised as compound literal */
342
343
  /*
344
   *  Default to something sensible
345
   *  instead of locking the same indefinitely.
346
   */
347
0
  if (!fr_time_delta_ispos(state->args.exec.timeout)) state->args.exec.timeout = fr_time_delta_from_sec(EXEC_TIMEOUT);
348
349
0
  fr_value_box_list_init(&state->list);
350
351
0
  return 0;
352
0
}
353
354
static void unlang_tmpl_dump(request_t *request, unlang_stack_frame_t *frame)
355
0
{
356
0
  unlang_frame_state_tmpl_t *state = talloc_get_type_abort(frame->state, unlang_frame_state_tmpl_t);
357
358
0
  if (state->vpt) {
359
0
    RDEBUG("tmpl           %s", state->vpt->name);
360
0
  } else {
361
0
    unlang_tmpl_t *ut = unlang_generic_to_tmpl(frame->instruction);
362
0
    RDEBUG("tmpl           %s", ut->tmpl->name);
363
0
  }
364
0
}
365
366
void unlang_tmpl_init(void)
367
4
{
368
4
  unlang_register(&(unlang_op_t){
369
4
      .name = "tmpl",
370
4
      .type = UNLANG_TYPE_TMPL,
371
4
      .flag = UNLANG_OP_FLAG_INTERNAL,
372
373
4
      .interpret = unlang_tmpl,
374
4
      .signal = unlang_tmpl_signal,
375
4
      .dump = unlang_tmpl_dump,
376
377
4
      .unlang_size = sizeof(unlang_tmpl_t),
378
4
      .unlang_name = "unlang_tmpl_t",
379
380
4
      .frame_state_size = sizeof(unlang_frame_state_tmpl_t),
381
4
      .frame_state_type = "unlang_frame_state_tmpl_t",
382
4
    });
383
4
}