Coverage Report

Created: 2026-09-28 07:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/sudo/plugins/sudoers/match_command.c
Line
Count
Source
1
/*
2
 * SPDX-License-Identifier: ISC
3
 *
4
 * Copyright (c) 1996, 1998-2005, 2007-2023
5
 *  Todd C. Miller <Todd.Miller@sudo.ws>
6
 *
7
 * Permission to use, copy, modify, and distribute this software for any
8
 * purpose with or without fee is hereby granted, provided that the above
9
 * copyright notice and this permission notice appear in all copies.
10
 *
11
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18
 *
19
 * Sponsored in part by the Defense Advanced Research Projects
20
 * Agency (DARPA) and Air Force Research Laboratory, Air Force
21
 * Materiel Command, USAF, under agreement number F39502-99-1-0512.
22
 */
23
24
#include <config.h>
25
26
#include <sys/stat.h>
27
#include <stdio.h>
28
#include <stdlib.h>
29
#include <string.h>
30
#include <unistd.h>
31
#ifndef SUDOERS_NAME_MATCH
32
# ifdef HAVE_GLOB
33
#  include <glob.h>
34
# else
35
#  include <compat/glob.h>
36
# endif /* HAVE_GLOB */
37
#endif /* SUDOERS_NAME_MATCH */
38
#include <dirent.h>
39
#include <fcntl.h>
40
#include <errno.h>
41
#ifdef HAVE_FNMATCH
42
# include <fnmatch.h>
43
#else
44
# include <compat/fnmatch.h>
45
#endif /* HAVE_FNMATCH */
46
#include <regex.h>
47
48
#include <sudoers.h>
49
#include <gram.h>
50
51
#if !defined(O_EXEC) && defined(O_PATH)
52
0
# define O_EXEC O_PATH
53
#endif
54
55
static int
56
regex_matches(const char *pattern, const char *str)
57
0
{
58
0
    const char *errstr;
59
0
    regex_t re;
60
0
    int ret;
61
0
    debug_decl(regex_matches, SUDOERS_DEBUG_MATCH);
62
63
0
    if (!sudo_regex_compile(&re, pattern, &errstr)) {
64
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
65
0
      "unable to compile regular expression \"%s\": %s",
66
0
      pattern, errstr);
67
0
  debug_return_int(DENY);
68
0
    }
69
70
0
    if (regexec(&re, str, 0, NULL, 0) == 0)
71
0
  ret = ALLOW;
72
0
    else
73
0
  ret = DENY;
74
0
    regfree(&re);
75
76
0
    debug_return_int(ret);
77
0
}
78
79
static int
80
command_args_match(struct sudoers_context *ctx, const char *sudoers_cmnd,
81
    const char *sudoers_args)
82
0
{
83
0
    const char *args = ctx->user.cmnd_args ? ctx->user.cmnd_args : "";
84
0
    int flags = 0;
85
0
    debug_decl(command_args_match, SUDOERS_DEBUG_MATCH);
86
87
    /*
88
     * If no args specified in sudoers, any user args are allowed.
89
     * If the empty string is specified in sudoers, no user args are allowed.
90
     */
91
0
    if (sudoers_args == NULL)
92
0
  debug_return_int(ALLOW);
93
0
    if (strcmp("\"\"", sudoers_args) == 0)
94
0
  debug_return_int(ctx->user.cmnd_args ? DENY : ALLOW);
95
96
    /*
97
     * If args are specified in sudoers, they must match the user args.
98
     * Args are matched either as a regular expression or glob pattern.
99
     */
100
0
    if (sudoers_args[0] == '^') {
101
0
  size_t len = strlen(sudoers_args);
102
0
  if (len > 0 && sudoers_args[len - 1] == '$')
103
0
      debug_return_int(regex_matches(sudoers_args, args));
104
0
    }
105
106
    /* If running as sudoedit, all args are assumed to be paths. */
107
0
    if (strcmp(sudoers_cmnd, "sudoedit") == 0)
108
0
  flags = FNM_PATHNAME;
109
0
    if (fnmatch(sudoers_args, args, flags) == 0)
110
0
  debug_return_int(ALLOW);
111
0
    debug_return_int(DENY);
112
0
}
113
114
#ifndef SUDOERS_NAME_MATCH
115
/*
116
 * Stat file by fd is possible, else by path.
117
 * Returns true on success, else false.
118
 */
119
static bool
120
do_stat(int fd, const char *path, const char *runchroot, struct stat *sb)
121
{
122
    char pathbuf[PATH_MAX];
123
    bool ret;
124
    debug_decl(do_stat, SUDOERS_DEBUG_MATCH);
125
126
    if (fd != -1) {
127
  ret = fstat(fd, sb) == 0;
128
    } else {
129
  /* Make path relative to the new root, if any. */
130
  if (runchroot != NULL) {
131
      /* XXX - handle symlinks and '..' in path outside chroot */
132
      const int len =
133
    snprintf(pathbuf, sizeof(pathbuf), "%s%s", runchroot, path);
134
      if (len >= ssizeof(pathbuf)) {
135
    errno = ENAMETOOLONG;
136
    debug_return_bool(false);
137
      }
138
      path = pathbuf;
139
  }
140
  ret = stat(path, sb) == 0;
141
    }
142
    debug_return_bool(ret);
143
}
144
#endif /* SUDOERS_NAME_MATCH */
145
146
/*
147
 * Check whether the fd refers to a shell script with a "#!" shebang.
148
 */
149
static bool
150
is_script(int fd)
151
0
{
152
0
    bool ret = false;
153
0
    char magic[2];
154
0
    debug_decl(is_script, SUDOERS_DEBUG_MATCH);
155
156
0
    if (pread(fd, magic, 2, 0) == 2) {
157
0
  if (magic[0] == '#' && magic[1] == '!')
158
0
      ret = true;
159
0
    }
160
0
    debug_return_bool(ret);
161
0
}
162
163
/*
164
 * Open path if fdexec is enabled or if a digest is present.
165
 * Returns false on error, else true.
166
 */
167
static bool
168
open_cmnd(const char *path, const char *runchroot,
169
    const struct command_digest_list *digests, int *fdp)
170
0
{
171
0
    int fd;
172
0
    char pathbuf[PATH_MAX];
173
0
    debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH);
174
175
    /* Only open the file for fdexec or for digest matching. */
176
0
    if (def_fdexec != always && TAILQ_EMPTY(digests))
177
0
  debug_return_bool(true);
178
179
    /* Make path relative to the new root, if any. */
180
0
    if (runchroot != NULL) {
181
  /* XXX - handle symlinks and '..' in path outside chroot */
182
0
  const int len =
183
0
      snprintf(pathbuf, sizeof(pathbuf), "%s%s", runchroot, path);
184
0
  if (len >= ssizeof(pathbuf)) {
185
0
      errno = ENAMETOOLONG;
186
0
      debug_return_bool(false);
187
0
  }
188
0
  path = pathbuf;
189
0
    }
190
191
0
    fd = open(path, O_RDONLY|O_NONBLOCK);
192
0
# ifdef O_EXEC
193
0
    if (fd == -1 && errno == EACCES && TAILQ_EMPTY(digests)) {
194
  /* Try again with O_EXEC if no digest is specified. */
195
0
  const int saved_errno = errno;
196
0
  if ((fd = open(path, O_EXEC)) == -1)
197
0
      errno = saved_errno;
198
0
    }
199
0
# endif
200
0
    if (fd == -1)
201
0
  debug_return_bool(false);
202
203
0
    (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
204
0
    *fdp = fd;
205
0
    debug_return_bool(true);
206
0
}
207
208
static void
209
set_cmnd_fd(struct sudoers_context *ctx, int fd)
210
0
{
211
0
    debug_decl(set_cmnd_fd, SUDOERS_DEBUG_MATCH);
212
213
0
    if (ctx->runas.execfd != -1)
214
0
  close(ctx->runas.execfd);
215
216
0
    if (fd != -1) {
217
0
  if (def_fdexec == never) {
218
      /* Never use fexedcve() */
219
0
      close(fd);
220
0
      fd = -1;
221
0
  } else if (is_script(fd)) {
222
0
      char fdpath[PATH_MAX];
223
0
      struct stat sb;
224
0
      int flags;
225
226
      /* We can only use fexecve() on a script if /dev/fd/N exists. */
227
0
      (void)snprintf(fdpath, sizeof(fdpath), "/dev/fd/%d", fd);
228
0
      if (stat(fdpath, &sb) != 0) {
229
    /* Missing /dev/fd file, can't use fexecve(). */
230
0
    close(fd);
231
0
    fd = -1;
232
0
      } else {
233
    /*
234
     * Shell scripts go through namei twice so we can't have the
235
     * close on exec flag set on the fd for fexecve(2).
236
     */
237
0
    flags = fcntl(fd, F_GETFD) & ~FD_CLOEXEC;
238
0
    (void)fcntl(fd, F_SETFD, flags);
239
0
      }
240
0
  }
241
0
    }
242
243
0
    ctx->runas.execfd = fd;
244
245
0
    debug_return;
246
0
}
247
248
#ifndef SUDOERS_NAME_MATCH
249
/*
250
 * Return true if ctx->user.cmnd names one of the inodes in dir, else false.
251
 */
252
static int
253
command_matches_dir(struct sudoers_context *ctx, const char *sudoers_dir,
254
    size_t dlen, const char *runchroot,
255
    const struct command_digest_list *digests)
256
{
257
    struct stat sudoers_stat;
258
    char path[PATH_MAX], sdbuf[PATH_MAX];
259
    size_t chrootlen = 0;
260
    int len, fd = -1;
261
    debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH);
262
263
    /* Make sudoers_dir relative to the new root, if any. */
264
    if (runchroot != NULL) {
265
  /* XXX - handle symlinks and '..' in path outside chroot */
266
  len = snprintf(sdbuf, sizeof(sdbuf), "%s%s", runchroot, sudoers_dir);
267
  if (len >= ssizeof(sdbuf)) {
268
      errno = ENAMETOOLONG;
269
      sudo_warn("%s%s", runchroot, sudoers_dir);
270
      goto bad;
271
  }
272
  sudoers_dir = sdbuf;
273
  chrootlen = strlen(runchroot);
274
    }
275
276
    /* Compare the canonicalized directories, if possible. */
277
    if (ctx->user.cmnd_dir != NULL) {
278
  char *resolved = canon_path(sudoers_dir);
279
  if (resolved != NULL) {
280
      if (strcmp(resolved, ctx->user.cmnd_dir) != 0) {
281
    canon_path_free(resolved);
282
    goto bad;
283
      }
284
      canon_path_free(resolved);
285
  }
286
    }
287
288
    /* Check for command in sudoers_dir. */
289
    len = snprintf(path, sizeof(path), "%s/%s", sudoers_dir, ctx->user.cmnd_base);
290
    if (len < 0 || len >= ssizeof(path))
291
  goto bad;
292
293
    /* Open the file for fdexec or for digest matching. */
294
    if (!open_cmnd(path, NULL, digests, &fd))
295
  goto bad;
296
    if (!do_stat(fd, path, NULL, &sudoers_stat))
297
  goto bad;
298
299
    if (ctx->user.cmnd_stat == NULL ||
300
  (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
301
  ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
302
  /* path is already relative to runchroot */
303
  if (digest_matches(fd, path, NULL, digests) != ALLOW)
304
      goto bad;
305
  free(ctx->runas.cmnd);
306
  if ((ctx->runas.cmnd = strdup(path + chrootlen)) == NULL) {
307
      sudo_warnx(U_("%s: %s"), __func__,
308
    U_("unable to allocate memory"));
309
      goto bad;
310
  }
311
  set_cmnd_fd(ctx, fd);
312
  debug_return_int(ALLOW);
313
    }
314
315
bad:
316
    if (fd != -1)
317
  close(fd);
318
    debug_return_int(DENY);
319
}
320
#else /* SUDOERS_NAME_MATCH */
321
/*
322
 * Return true if ctx->user.cmnd names one of the inodes in dir, else false.
323
 */
324
static int
325
command_matches_dir(struct sudoers_context *ctx, const char *sudoers_dir,
326
    size_t dlen, const char *runchroot,
327
    const struct command_digest_list *digests)
328
0
{
329
0
    int fd = -1;
330
0
    debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH);
331
332
    /* Match ctx->user.cmnd against sudoers_dir. */
333
0
    if (strncmp(ctx->user.cmnd, sudoers_dir, dlen) != 0 || ctx->user.cmnd[dlen] != '/')
334
0
  goto bad;
335
336
    /* Make sure ctx->user.cmnd is not in a subdir of sudoers_dir. */
337
0
    if (strchr(ctx->user.cmnd + dlen + 1, '/') != NULL)
338
0
  goto bad;
339
340
    /* Open the file for fdexec or for digest matching. */
341
0
    if (!open_cmnd(ctx->user.cmnd, runchroot, digests, &fd))
342
0
  goto bad;
343
0
    if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
344
0
  goto bad;
345
0
    set_cmnd_fd(ctx, fd);
346
347
0
    debug_return_int(ALLOW);
348
0
bad:
349
0
    if (fd != -1)
350
0
  close(fd);
351
0
    debug_return_int(DENY);
352
0
}
353
#endif /* SUDOERS_NAME_MATCH */
354
355
static int
356
command_matches_all(struct sudoers_context *ctx, const char *runchroot,
357
    const struct command_digest_list *digests)
358
0
{
359
#ifndef SUDOERS_NAME_MATCH
360
    struct stat sb;
361
#endif
362
0
    int fd = -1;
363
0
    debug_decl(command_matches_all, SUDOERS_DEBUG_MATCH);
364
365
0
    if (strchr(ctx->user.cmnd, '/') != NULL) {
366
#ifndef SUDOERS_NAME_MATCH
367
  /* Open the file for fdexec or for digest matching. */
368
  bool open_error = !open_cmnd(ctx->user.cmnd, runchroot, digests, &fd);
369
370
  /* A non-existent file is not an error for "sudo ALL". */
371
  if (do_stat(fd, ctx->user.cmnd, runchroot, &sb)) {
372
      if (open_error) {
373
    /* File exists but we couldn't open it above? */
374
    goto bad;
375
      }
376
  }
377
#else
378
  /* Open the file for fdexec or for digest matching. */
379
0
  (void)open_cmnd(ctx->user.cmnd, runchroot, digests, &fd);
380
0
#endif
381
0
    }
382
383
    /* Check digest of ctx->user.cmnd since we have no sudoers_cmnd for ALL. */
384
0
    if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
385
0
  goto bad;
386
0
    set_cmnd_fd(ctx, fd);
387
388
    /* No need to set ctx->runas.cmnd for ALL. */
389
0
    debug_return_int(ALLOW);
390
0
bad:
391
0
    if (fd != -1)
392
0
  close(fd);
393
0
    debug_return_int(DENY);
394
0
}
395
396
static int
397
command_matches_fnmatch(struct sudoers_context *ctx, const char *sudoers_cmnd,
398
    const char *sudoers_args, const char *runchroot,
399
    const struct command_digest_list *digests)
400
0
{
401
0
    const char *cmnd = ctx->user.cmnd;
402
0
    char buf[PATH_MAX];
403
0
    int len, fd = -1;
404
#ifndef SUDOERS_NAME_MATCH
405
    struct stat sb;
406
#endif
407
0
    debug_decl(command_matches_fnmatch, SUDOERS_DEBUG_MATCH);
408
409
    /*
410
     * Return ALLOW if fnmatch(3) succeeds AND
411
     *  a) there are no args in sudoers OR
412
     *  b) there are no args on command line and none required by sudoers OR
413
     *  c) there are args in sudoers and on command line and they match
414
     *     else return DENY.
415
     *
416
     * Neither sudoers_cmnd nor user_cmnd are relative to runchroot.
417
     * We do not attempt to match a relative path unless there is a
418
     * canonicalized version.
419
     */
420
0
    if (cmnd[0] != '/' || sudo_contains_dot_dot(cmnd) ||
421
0
      fnmatch(sudoers_cmnd, cmnd, FNM_PATHNAME) != 0) {
422
  /* No match, retry using the canonicalized path (if possible). */
423
0
  if (ctx->user.cmnd_dir == NULL)
424
0
      debug_return_int(DENY);
425
0
  len = snprintf(buf, sizeof(buf), "%s/%s", ctx->user.cmnd_dir,
426
0
      ctx->user.cmnd_base);
427
0
  if (len < 0 || len >= ssizeof(buf))
428
0
      debug_return_int(DENY);
429
0
  cmnd = buf;
430
0
  if (fnmatch(sudoers_cmnd, cmnd, FNM_PATHNAME) != 0)
431
0
      debug_return_int(DENY);
432
0
    }
433
434
0
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
435
  /* Open the file for fdexec or for digest matching. */
436
0
  if (!open_cmnd(cmnd, runchroot, digests, &fd))
437
0
      goto bad;
438
#ifndef SUDOERS_NAME_MATCH
439
  if (!do_stat(fd, cmnd, runchroot, &sb))
440
      goto bad;
441
#endif
442
  /* Check digest of cmnd since sudoers_cmnd is a pattern. */
443
0
  if (digest_matches(fd, cmnd, runchroot, digests) != ALLOW)
444
0
      goto bad;
445
446
  /* Successful match. */
447
0
  free(ctx->runas.cmnd);
448
0
  if ((ctx->runas.cmnd = strdup(cmnd)) == NULL) {
449
0
      sudo_warnx(U_("%s: %s"), __func__,
450
0
    U_("unable to allocate memory"));
451
0
      debug_return_int(DENY);
452
0
  }
453
0
  set_cmnd_fd(ctx, fd);
454
0
  debug_return_int(ALLOW);
455
0
bad:
456
0
  if (fd != -1)
457
0
      close(fd);
458
0
    }
459
0
    debug_return_int(DENY);
460
0
}
461
462
static int
463
command_matches_regex(struct sudoers_context *ctx, const char *sudoers_cmnd,
464
    const char *sudoers_args, const char *runchroot,
465
    const struct command_digest_list *digests)
466
0
{
467
0
    const char *cmnd = ctx->user.cmnd;
468
0
    char buf[PATH_MAX];
469
0
    int len, fd = -1;
470
#ifndef SUDOERS_NAME_MATCH
471
    struct stat sb;
472
#endif
473
0
    debug_decl(command_matches_regex, SUDOERS_DEBUG_MATCH);
474
475
    /*
476
     * Return ALLOW if sudoers_cmnd regex matches cmnd AND
477
     *  a) there are no args in sudoers OR
478
     *  b) there are no args on command line and none required by sudoers OR
479
     *  c) there are args in sudoers and on command line and they match
480
     *     else return DENY.
481
     *
482
     * Neither sudoers_cmnd nor user_cmnd are relative to runchroot.
483
     */
484
0
    if (cmnd[0] != '/' || sudo_contains_dot_dot(cmnd) ||
485
0
      regex_matches(sudoers_cmnd, cmnd) != ALLOW) {
486
  /* No match, retry using the canonicalized path (if possible). */
487
0
  if (ctx->user.cmnd_dir == NULL)
488
0
      debug_return_int(DENY);
489
0
  len = snprintf(buf, sizeof(buf), "%s/%s", ctx->user.cmnd_dir,
490
0
      ctx->user.cmnd_base);
491
0
  if (len < 0 || len >= ssizeof(buf))
492
0
      debug_return_int(DENY);
493
0
  cmnd = buf;
494
0
  if (regex_matches(sudoers_cmnd, cmnd) != ALLOW)
495
0
      debug_return_int(DENY);
496
0
    }
497
498
0
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
499
  /* Open the file for fdexec or for digest matching. */
500
0
  if (!open_cmnd(cmnd, runchroot, digests, &fd))
501
0
      goto bad;
502
#ifndef SUDOERS_NAME_MATCH
503
  if (!do_stat(fd, cmnd, runchroot, &sb))
504
      goto bad;
505
#endif
506
  /* Check digest of cmnd since sudoers_cmnd is a regex. */
507
0
  if (digest_matches(fd, cmnd, runchroot, digests) != ALLOW)
508
0
      goto bad;
509
510
  /* Successful match. */
511
0
  free(ctx->runas.cmnd);
512
0
  if ((ctx->runas.cmnd = strdup(cmnd)) == NULL) {
513
0
      sudo_warnx(U_("%s: %s"), __func__,
514
0
    U_("unable to allocate memory"));
515
0
      debug_return_int(DENY);
516
0
  }
517
0
  set_cmnd_fd(ctx, fd);
518
0
  debug_return_int(ALLOW);
519
0
bad:
520
0
  if (fd != -1)
521
0
      close(fd);
522
0
    }
523
0
    debug_return_int(DENY);
524
0
}
525
526
#ifndef SUDOERS_NAME_MATCH
527
static int
528
command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd,
529
    const char *sudoers_args, const char *runchroot,
530
    const struct command_digest_list *digests)
531
{
532
    struct stat sudoers_stat;
533
    bool bad_digest = false;
534
    char **ap, *base, *cp;
535
    char pathbuf[PATH_MAX];
536
    int fd = -1;
537
    size_t dlen, chrootlen = 0;
538
    glob_t gl;
539
    debug_decl(command_matches_glob, SUDOERS_DEBUG_MATCH);
540
541
    /* Make sudoers_cmnd relative to the new root, if any. */
542
    if (runchroot != NULL) {
543
  /* XXX - handle symlinks and '..' in path outside chroot */
544
  const int len =
545
      snprintf(pathbuf, sizeof(pathbuf), "%s%s", runchroot, sudoers_cmnd);
546
  if (len >= ssizeof(pathbuf)) {
547
      errno = ENAMETOOLONG;
548
      sudo_warn("%s%s", runchroot, sudoers_cmnd);
549
      debug_return_int(DENY);
550
  }
551
  if (has_meta(runchroot)) {
552
      /* Do not allow meta characters in runchroot. */
553
      errno = EINVAL;
554
      sudo_warn("%s%s", runchroot, sudoers_cmnd);
555
      debug_return_int(DENY);
556
  }
557
  sudoers_cmnd = pathbuf;
558
  chrootlen = strlen(runchroot);
559
    }
560
561
    /*
562
     * First check to see if we can avoid the call to glob(3).
563
     * Short circuit if there are no meta chars in the command itself
564
     * and ctx->user.cmnd_base and basename(sudoers_cmnd) don't match.
565
     */
566
    dlen = strlen(sudoers_cmnd);
567
    if (sudoers_cmnd[dlen - 1] != '/') {
568
  base = sudo_basename(sudoers_cmnd);
569
  if (!has_meta(base) && strcmp(ctx->user.cmnd_base, base) != 0)
570
      debug_return_int(DENY);
571
    }
572
573
    /*
574
     * Return ALLOW if we find a match in the glob(3) results AND
575
     *  a) there are no args in sudoers OR
576
     *  b) there are no args on command line and none required by sudoers OR
577
     *  c) there are args in sudoers and on command line and they match
578
     * else return DENY.
579
     */
580
    if (glob(sudoers_cmnd, GLOB_NOSORT, NULL, &gl) != 0 || gl.gl_pathc == 0) {
581
  globfree(&gl);
582
  debug_return_int(DENY);
583
    }
584
585
    /* If ctx->user.cmnd is fully-qualified, check for an exact match. */
586
    if (ctx->user.cmnd[0] == '/') {
587
  for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
588
      if (fd != -1) {
589
    close(fd);
590
    fd = -1;
591
      }
592
      /* Remove the runchroot, if any. */
593
      if (runchroot != NULL) {
594
    if (strncmp(cp, runchroot, chrootlen) != 0)
595
        continue;
596
    cp += chrootlen;
597
      }
598
599
      if (strcmp(cp, ctx->user.cmnd) != 0)
600
    continue;
601
      /* Open the file for fdexec or for digest matching. */
602
      if (!open_cmnd(cp, runchroot, digests, &fd))
603
    continue;
604
      if (!do_stat(fd, cp, runchroot, &sudoers_stat))
605
    continue;
606
      if (ctx->user.cmnd_stat == NULL ||
607
    (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
608
    ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
609
    /* There could be multiple matches, check digest early. */
610
    if (digest_matches(fd, cp, runchroot, digests) != ALLOW) {
611
        bad_digest = true;
612
        continue;
613
    }
614
    free(ctx->runas.cmnd);
615
    if ((ctx->runas.cmnd = strdup(cp)) == NULL) {
616
        sudo_warnx(U_("%s: %s"), __func__,
617
      U_("unable to allocate memory"));
618
        cp = NULL;    /* fail closed */
619
    }
620
      } else {
621
    /* Paths match, but st_dev and st_ino are different. */
622
    cp = NULL;    /* fail closed */
623
      }
624
      goto done;
625
  }
626
    }
627
    /* No exact match, compare basename, cmnd_dir, st_dev and st_ino. */
628
    if (!bad_digest) {
629
  for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
630
      if (fd != -1) {
631
    close(fd);
632
    fd = -1;
633
      }
634
      /* Remove the runchroot, if any. */
635
      if (runchroot != NULL) {
636
    if (strncmp(cp, runchroot, chrootlen) != 0)
637
        continue;
638
    cp += chrootlen;
639
      }
640
641
      /* If it ends in '/' it is a directory spec. */
642
      dlen = strlen(cp);
643
      if (cp[dlen - 1] == '/') {
644
    if (command_matches_dir(ctx, cp, dlen, runchroot, digests) == ALLOW) {
645
        globfree(&gl);
646
        debug_return_int(ALLOW);
647
    }
648
    continue;
649
      }
650
651
      /* Only proceed if ctx->user.cmnd_base and basename(cp) match */
652
      base = sudo_basename(cp);
653
      if (strcmp(ctx->user.cmnd_base, base) != 0)
654
    continue;
655
656
      /* Compare the canonicalized parent directories, if possible. */
657
      if (ctx->user.cmnd_dir != NULL) {
658
    char *slash = strrchr(cp, '/');
659
    if (slash != NULL) {
660
        char *resolved;
661
        *slash = '\0';
662
        resolved = canon_path(cp);
663
        *slash = '/';
664
        if (resolved != NULL) {
665
      /* Canonicalized directories must match. */
666
      int result = strcmp(resolved, ctx->user.cmnd_dir);
667
      canon_path_free(resolved);
668
      if (result != 0)
669
          continue;
670
        }
671
    }
672
      }
673
674
      /* Open the file for fdexec or for digest matching. */
675
      if (!open_cmnd(cp, runchroot, digests, &fd))
676
    continue;
677
      if (!do_stat(fd, cp, runchroot, &sudoers_stat))
678
    continue;
679
      if (ctx->user.cmnd_stat == NULL ||
680
    (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
681
    ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
682
    if (digest_matches(fd, cp, runchroot, digests) != ALLOW)
683
        continue;
684
    free(ctx->runas.cmnd);
685
    if ((ctx->runas.cmnd = strdup(cp)) == NULL) {
686
        sudo_warnx(U_("%s: %s"), __func__,
687
      U_("unable to allocate memory"));
688
        cp = NULL;    /* fail closed */
689
    }
690
    goto done;
691
      }
692
  }
693
    }
694
done:
695
    globfree(&gl);
696
    if (cp != NULL) {
697
  if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
698
      /* ctx->runas.cmnd was set above. */
699
      set_cmnd_fd(ctx, fd);
700
      debug_return_int(ALLOW);
701
  }
702
    }
703
    if (fd != -1)
704
  close(fd);
705
    debug_return_int(DENY);
706
}
707
708
static int
709
command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd,
710
    const char *sudoers_args, const char *runchroot,
711
    const struct command_digest_list *digests)
712
{
713
    struct stat sudoers_stat;
714
    const char *base;
715
    size_t dlen;
716
    int fd = -1;
717
    debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH);
718
719
    /* If it ends in '/' it is a directory spec. */
720
    dlen = strlen(sudoers_cmnd);
721
    if (sudoers_cmnd[dlen - 1] == '/') {
722
  debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen,
723
      runchroot, digests));
724
    }
725
726
    /* Only proceed if ctx->user.cmnd_base and basename(sudoers_cmnd) match */
727
    base = sudo_basename(sudoers_cmnd);
728
    if (strcmp(ctx->user.cmnd_base, base) != 0)
729
  debug_return_int(DENY);
730
731
    /* Compare the canonicalized parent directories, if possible. */
732
    if (ctx->user.cmnd_dir != NULL) {
733
  const char *slash = strrchr(sudoers_cmnd, '/');
734
  if (slash != NULL) {
735
      char sudoers_cmnd_dir[PATH_MAX], *resolved;
736
      const size_t len = (size_t)(slash - sudoers_cmnd);
737
      if (len >= sizeof(sudoers_cmnd_dir))
738
    goto bad;
739
      if (len != 0)
740
    memcpy(sudoers_cmnd_dir, sudoers_cmnd, len);
741
      sudoers_cmnd_dir[len] = '\0';
742
      resolved = canon_path(sudoers_cmnd_dir);
743
      if (resolved != NULL) {
744
    if (strcmp(resolved, ctx->user.cmnd_dir) != 0) {
745
        canon_path_free(resolved);
746
        goto bad;
747
    }
748
    canon_path_free(resolved);
749
      }
750
  }
751
    }
752
753
    /* Open the file for fdexec or for digest matching. */
754
    if (!open_cmnd(sudoers_cmnd, runchroot, digests, &fd))
755
  goto bad;
756
757
    /*
758
     * Return true if command matches AND
759
     *  a) there are no args in sudoers OR
760
     *  b) there are no args on command line and none req by sudoers OR
761
     *  c) there are args in sudoers and on command line and they match
762
     *  d) there is a digest and it matches
763
     */
764
    if (ctx->user.cmnd_stat != NULL && do_stat(fd, sudoers_cmnd, runchroot, &sudoers_stat)) {
765
  if (ctx->user.cmnd_stat->st_dev != sudoers_stat.st_dev ||
766
      ctx->user.cmnd_stat->st_ino != sudoers_stat.st_ino)
767
      goto bad;
768
    } else {
769
  /* Either user or sudoers command does not exist, match by name. */
770
  if (strcmp(ctx->user.cmnd, sudoers_cmnd) != 0)
771
      goto bad;
772
    }
773
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) != ALLOW)
774
  goto bad;
775
    if (digest_matches(fd, sudoers_cmnd, runchroot, digests) != ALLOW) {
776
  /* XXX - log functions not available but we should log very loudly */
777
  goto bad;
778
    }
779
    free(ctx->runas.cmnd);
780
    if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) {
781
  sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
782
  goto bad;
783
    }
784
    set_cmnd_fd(ctx, fd);
785
    debug_return_int(ALLOW);
786
bad:
787
    if (fd != -1)
788
  close(fd);
789
    debug_return_int(DENY);
790
}
791
#else /* SUDOERS_NAME_MATCH */
792
static int
793
command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd,
794
    const char *sudoers_args, const char *runchroot,
795
    const struct command_digest_list *digests)
796
0
{
797
0
    return command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args, runchroot,
798
0
  digests);
799
0
}
800
801
static int
802
command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd,
803
    const char *sudoers_args, const char *runchroot,
804
    const struct command_digest_list *digests)
805
0
{
806
0
    size_t dlen;
807
0
    int fd = -1;
808
0
    debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH);
809
810
    /* If it ends in '/' it is a directory spec. */
811
0
    dlen = strlen(sudoers_cmnd);
812
0
    if (sudoers_cmnd[dlen - 1] == '/') {
813
0
  debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen, runchroot,
814
0
      digests));
815
0
    }
816
817
0
    if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0) {
818
0
  if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
819
      /* Open the file for fdexec or for digest matching. */
820
0
      if (!open_cmnd(ctx->user.cmnd, runchroot, digests, &fd))
821
0
    goto bad;
822
0
      if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
823
0
    goto bad;
824
825
      /* Successful match. */
826
0
      free(ctx->runas.cmnd);
827
0
      if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) {
828
0
    sudo_warnx(U_("%s: %s"), __func__,
829
0
        U_("unable to allocate memory"));
830
0
    goto bad;
831
0
      }
832
0
      set_cmnd_fd(ctx, fd);
833
0
      debug_return_int(ALLOW);
834
0
  }
835
0
    }
836
0
bad:
837
0
    if (fd != -1)
838
0
  close(fd);
839
0
    debug_return_int(DENY);
840
0
}
841
#endif /* SUDOERS_NAME_MATCH */
842
843
/*
844
 * If path doesn't end in /, return ALLOW iff cmnd & path name the same inode;
845
 * otherwise, return ALLOW if ctx->user.cmnd names one of the inodes in path.
846
 * Returns DENY on failure.
847
 */
848
int
849
command_matches(struct sudoers_context *ctx, const char *sudoers_cmnd,
850
    const char *sudoers_args, const char *runchroot, struct cmnd_info *info,
851
    const struct command_digest_list *digests)
852
0
{
853
0
    char *saved_user_cmnd = NULL;
854
0
    struct stat saved_user_stat;
855
0
    int ret = DENY;
856
0
    debug_decl(command_matches, SUDOERS_DEBUG_MATCH);
857
858
0
    if (ctx->runas.chroot != NULL) {
859
0
  if (runchroot != NULL && strcmp(runchroot, "*") != 0 &&
860
0
    strcmp(runchroot, ctx->runas.chroot) != 0) {
861
      /* CHROOT mismatch */
862
0
      goto done;
863
0
  }
864
  /* User-specified runchroot (cmnd_stat already set appropriately). */
865
0
  runchroot = ctx->runas.chroot;
866
0
    } else if (runchroot == NULL) {
867
  /* No rule-specific runchroot, use global (cmnd_stat already set). */
868
0
  if (def_runchroot != NULL && strcmp(def_runchroot, "*") != '\0')
869
0
      runchroot = def_runchroot;
870
0
    } else {
871
  /* Rule-specific runchroot, must reset cmnd and cmnd_stat. */
872
0
  int status;
873
874
  /* Save old ctx->user.cmnd first, set_cmnd_path() will free it. */
875
0
  saved_user_cmnd = ctx->user.cmnd;
876
0
  ctx->user.cmnd = NULL;
877
0
  if (ctx->user.cmnd_stat != NULL)
878
0
      saved_user_stat = *ctx->user.cmnd_stat;
879
0
  status = set_cmnd_path(ctx, runchroot);
880
0
  if (status != FOUND) {
881
0
      ctx->user.cmnd = saved_user_cmnd;
882
0
      saved_user_cmnd = NULL;
883
0
  }
884
0
  if (info != NULL)
885
0
      info->status = status;
886
0
    }
887
888
0
    if (sudoers_cmnd == NULL) {
889
0
  sudoers_cmnd = "ALL";
890
0
  ret = command_matches_all(ctx, runchroot, digests);
891
0
  goto done;
892
0
    }
893
894
    /* Check for regular expressions first. */
895
0
    if (sudoers_cmnd[0] == '^') {
896
0
  ret = command_matches_regex(ctx, sudoers_cmnd, sudoers_args, runchroot,
897
0
      digests);
898
0
  goto done;
899
0
    }
900
901
    /* Check for pseudo-commands */
902
0
    if (sudoers_cmnd[0] != '/') {
903
  /*
904
   * Return true if sudoers_cmnd and cmnd match a pseudo-command AND
905
   *  a) there are no args in sudoers OR
906
   *  b) there are no args on command line and none req by sudoers OR
907
   *  c) there are args in sudoers and on command line and they match
908
   */
909
0
  if (strcmp(sudoers_cmnd, "list") == 0 ||
910
0
    strcmp(sudoers_cmnd, "sudoedit") == 0) {
911
0
      if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0 &&
912
0
        command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
913
    /* No need to set ctx->user.cmnd since cmnd == sudoers_cmnd */
914
0
    ret = ALLOW;
915
0
      }
916
0
  }
917
0
  goto done;
918
0
    }
919
920
0
    if (has_meta(sudoers_cmnd)) {
921
  /*
922
   * If sudoers_cmnd has meta characters in it, we need to
923
   * use glob(3) and/or fnmatch(3) to do the matching.
924
   */
925
0
  if (def_fast_glob) {
926
0
      ret = command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args,
927
0
    runchroot, digests);
928
0
  } else {
929
0
      ret = command_matches_glob(ctx, sudoers_cmnd, sudoers_args,
930
0
    runchroot, digests);
931
0
  }
932
0
    } else {
933
0
  ret = command_matches_normal(ctx, sudoers_cmnd, sudoers_args,
934
0
      runchroot, digests);
935
0
    }
936
0
done:
937
    /* Restore ctx->user.cmnd and ctx->user.cmnd_stat. */
938
0
    if (saved_user_cmnd != NULL) {
939
0
  if (info != NULL) {
940
0
      free(info->cmnd_path);
941
0
      info->cmnd_path = ctx->user.cmnd;
942
0
      if (ctx->user.cmnd_stat != NULL)
943
0
    info->cmnd_stat = *ctx->user.cmnd_stat;
944
0
  } else {
945
0
      free(ctx->user.cmnd);
946
0
  }
947
0
  ctx->user.cmnd = saved_user_cmnd;
948
0
  if (ctx->user.cmnd_stat != NULL)
949
0
      *ctx->user.cmnd_stat = saved_user_stat;
950
0
    }
951
0
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
952
0
  "user command \"%s%s%s\" matches sudoers command \"%s%s%s\"%s%s: %s",
953
0
  ctx->user.cmnd, ctx->user.cmnd_args ? " " : "",
954
0
  ctx->user.cmnd_args ? ctx->user.cmnd_args : "", sudoers_cmnd,
955
0
  sudoers_args ? " " : "", sudoers_args ? sudoers_args : "",
956
0
  runchroot ? ", chroot " : "", runchroot ? runchroot : "",
957
0
  ret == ALLOW ? "ALLOW" : "DENY");
958
0
    debug_return_int(ret);
959
0
}