Coverage Report

Created: 2025-08-26 06:57

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