Coverage Report

Created: 2025-10-13 06:08

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
11
{
83
11
    const char *args = ctx->user.cmnd_args ? ctx->user.cmnd_args : "";
84
11
    int flags = 0;
85
11
    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
11
    if (sudoers_args == NULL)
92
11
  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
32
{
152
32
    bool ret = false;
153
32
    char magic[2];
154
32
    debug_decl(is_script, SUDOERS_DEBUG_MATCH);
155
156
32
    if (pread(fd, magic, 2, 0) == 2) {
157
32
  if (magic[0] == '#' && magic[1] == '!')
158
0
      ret = true;
159
32
    }
160
32
    debug_return_bool(ret);
161
32
}
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
47
{
171
47
    int fd;
172
47
    char pathbuf[PATH_MAX];
173
47
    debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH);
174
175
    /* Only open the file for fdexec or for digest matching. */
176
47
    if (def_fdexec != always && TAILQ_EMPTY(digests))
177
2
  debug_return_bool(true);
178
179
    /* Make path relative to the new root, if any. */
180
45
    if (runchroot != NULL) {
181
  /* XXX - handle symlinks and '..' in path outside chroot */
182
2
  const int len =
183
2
      snprintf(pathbuf, sizeof(pathbuf), "%s%s", runchroot, path);
184
2
  if (len >= ssizeof(pathbuf)) {
185
0
      errno = ENAMETOOLONG;
186
0
      debug_return_bool(false);
187
0
  }
188
2
  path = pathbuf;
189
2
    }
190
191
45
    fd = open(path, O_RDONLY|O_NONBLOCK);
192
45
# ifdef O_EXEC
193
45
    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
45
# endif
200
45
    if (fd == -1)
201
2
  debug_return_bool(false);
202
203
43
    (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
204
43
    *fdp = fd;
205
43
    debug_return_bool(true);
206
43
}
207
208
static void
209
set_cmnd_fd(struct sudoers_context *ctx, int fd)
210
34
{
211
34
    debug_decl(set_cmnd_fd, SUDOERS_DEBUG_MATCH);
212
213
34
    if (ctx->runas.execfd != -1)
214
32
  close(ctx->runas.execfd);
215
216
34
    if (fd != -1) {
217
32
  if (def_fdexec == never) {
218
      /* Never use fexedcve() */
219
0
      close(fd);
220
0
      fd = -1;
221
32
  } 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
32
    }
242
243
34
    ctx->runas.execfd = fd;
244
245
34
    debug_return;
246
34
}
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
    int ret = DENY;
262
    debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH);
263
264
    /* Make sudoers_dir relative to the new root, if any. */
265
    if (runchroot != NULL) {
266
  /* XXX - handle symlinks and '..' in path outside chroot */
267
  len = snprintf(sdbuf, sizeof(sdbuf), "%s%s", runchroot, sudoers_dir);
268
  if (len >= ssizeof(sdbuf)) {
269
      errno = ENAMETOOLONG;
270
      sudo_warn("%s%s", runchroot, sudoers_dir);
271
      goto done;
272
  }
273
  sudoers_dir = sdbuf;
274
  chrootlen = strlen(runchroot);
275
    }
276
277
    /* Compare the canonicalized directories, if possible. */
278
    if (ctx->user.cmnd_dir != NULL) {
279
  char *resolved = canon_path(sudoers_dir);
280
  if (resolved != NULL) {
281
      if (strcmp(resolved, ctx->user.cmnd_dir) != 0) {
282
    canon_path_free(resolved);
283
    goto done;
284
      }
285
      canon_path_free(resolved);
286
  }
287
    }
288
289
    /* Check for command in sudoers_dir. */
290
    len = snprintf(path, sizeof(path), "%s/%s", sudoers_dir, ctx->user.cmnd_base);
291
    if (len < 0 || len >= ssizeof(path))
292
  goto done;
293
294
    /* Open the file for fdexec or for digest matching. */
295
    if (!open_cmnd(path, NULL, digests, &fd))
296
  goto done;
297
    if (!do_stat(fd, path, NULL, &sudoers_stat))
298
  goto done;
299
300
    if (ctx->user.cmnd_stat == NULL ||
301
  (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
302
  ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
303
  /* path is already relative to runchroot */
304
  if (digest_matches(fd, path, NULL, digests) != ALLOW)
305
      goto done;
306
  free(ctx->runas.cmnd);
307
  if ((ctx->runas.cmnd = strdup(path + chrootlen)) == NULL) {
308
      sudo_warnx(U_("%s: %s"), __func__,
309
    U_("unable to allocate memory"));
310
  }
311
  ret = ALLOW;
312
  goto done;
313
    }
314
    ret = DENY;
315
316
done:
317
    if (fd != -1)
318
  close(fd);
319
    debug_return_int(ret);
320
}
321
#else /* SUDOERS_NAME_MATCH */
322
/*
323
 * Return true if ctx->user.cmnd names one of the inodes in dir, else false.
324
 */
325
static int
326
command_matches_dir(struct sudoers_context *ctx, const char *sudoers_dir,
327
    size_t dlen, const char *runchroot,
328
    const struct command_digest_list *digests)
329
0
{
330
0
    int fd = -1;
331
0
    debug_decl(command_matches_dir, SUDOERS_DEBUG_MATCH);
332
333
    /* Match ctx->user.cmnd against sudoers_dir. */
334
0
    if (strncmp(ctx->user.cmnd, sudoers_dir, dlen) != 0 || ctx->user.cmnd[dlen] != '/')
335
0
  goto bad;
336
337
    /* Make sure ctx->user.cmnd is not in a subdir of sudoers_dir. */
338
0
    if (strchr(ctx->user.cmnd + dlen + 1, '\0') != NULL)
339
0
  goto bad;
340
341
    /* Open the file for fdexec or for digest matching. */
342
0
    if (!open_cmnd(ctx->user.cmnd, runchroot, digests, &fd))
343
0
  goto bad;
344
0
    if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
345
0
  goto bad;
346
0
    set_cmnd_fd(ctx, fd);
347
348
0
    debug_return_int(ALLOW);
349
0
bad:
350
0
    if (fd != -1)
351
0
  close(fd);
352
0
    debug_return_int(DENY);
353
0
}
354
#endif /* SUDOERS_NAME_MATCH */
355
356
static int
357
command_matches_all(struct sudoers_context *ctx, const char *runchroot,
358
    const struct command_digest_list *digests)
359
36
{
360
#ifndef SUDOERS_NAME_MATCH
361
    struct stat sb;
362
#endif
363
36
    int fd = -1;
364
36
    debug_decl(command_matches_all, SUDOERS_DEBUG_MATCH);
365
366
36
    if (strchr(ctx->user.cmnd, '/') != NULL) {
367
#ifndef SUDOERS_NAME_MATCH
368
  /* Open the file for fdexec or for digest matching. */
369
  bool open_error = !open_cmnd(ctx->user.cmnd, runchroot, digests, &fd);
370
371
  /* A non-existent file is not an error for "sudo ALL". */
372
  if (do_stat(fd, ctx->user.cmnd, runchroot, &sb)) {
373
      if (open_error) {
374
    /* File exists but we couldn't open it above? */
375
    goto bad;
376
      }
377
  }
378
#else
379
  /* Open the file for fdexec or for digest matching. */
380
36
  (void)open_cmnd(ctx->user.cmnd, runchroot, digests, &fd);
381
36
#endif
382
36
    }
383
384
    /* Check digest of ctx->user.cmnd since we have no sudoers_cmnd for ALL. */
385
36
    if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
386
10
  goto bad;
387
26
    set_cmnd_fd(ctx, fd);
388
389
    /* No need to set ctx->runas.cmnd for ALL. */
390
26
    debug_return_int(ALLOW);
391
10
bad:
392
10
    if (fd != -1)
393
8
  close(fd);
394
10
    debug_return_int(DENY);
395
10
}
396
397
static int
398
command_matches_fnmatch(struct sudoers_context *ctx, const char *sudoers_cmnd,
399
    const char *sudoers_args, const char *runchroot,
400
    const struct command_digest_list *digests)
401
19
{
402
19
    const char *cmnd = ctx->user.cmnd;
403
19
    char buf[PATH_MAX];
404
19
    int len, fd = -1;
405
#ifndef SUDOERS_NAME_MATCH
406
    struct stat sb;
407
#endif
408
19
    debug_decl(command_matches_fnmatch, SUDOERS_DEBUG_MATCH);
409
410
    /*
411
     * Return ALLOW if fnmatch(3) succeeds AND
412
     *  a) there are no args in sudoers OR
413
     *  b) there are no args on command line and none required by sudoers OR
414
     *  c) there are args in sudoers and on command line and they match
415
     *     else return DENY.
416
     *
417
     * Neither sudoers_cmnd nor user_cmnd are relative to runchroot.
418
     * We do not attempt to match a relative path unless there is a
419
     * canonicalized version.
420
     */
421
19
    if (cmnd[0] != '/' || fnmatch(sudoers_cmnd, cmnd, FNM_PATHNAME) != 0) {
422
  /* No match, retry using the canonicalized path (if possible). */
423
8
  if (ctx->user.cmnd_dir == NULL)
424
8
      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
11
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
435
  /* Open the file for fdexec or for digest matching. */
436
11
  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
11
  if (digest_matches(fd, cmnd, runchroot, digests) != ALLOW)
444
3
      goto bad;
445
8
  set_cmnd_fd(ctx, fd);
446
447
  /* No need to set ctx->runas.cmnd since cmnd matches sudoers_cmnd */
448
8
  debug_return_int(ALLOW);
449
3
bad:
450
3
  if (fd != -1)
451
3
      close(fd);
452
3
    }
453
3
    debug_return_int(DENY);
454
3
}
455
456
static int
457
command_matches_regex(struct sudoers_context *ctx, const char *sudoers_cmnd,
458
    const char *sudoers_args, const char *runchroot,
459
    const struct command_digest_list *digests)
460
0
{
461
0
    const char *cmnd = ctx->user.cmnd;
462
0
    char buf[PATH_MAX];
463
0
    int len, fd = -1;
464
#ifndef SUDOERS_NAME_MATCH
465
    struct stat sb;
466
#endif
467
0
    debug_decl(command_matches_regex, SUDOERS_DEBUG_MATCH);
468
469
    /*
470
     * Return ALLOW if sudoers_cmnd regex matches cmnd AND
471
     *  a) there are no args in sudoers OR
472
     *  b) there are no args on command line and none required by sudoers OR
473
     *  c) there are args in sudoers and on command line and they match
474
     *     else return DENY.
475
     *
476
     * Neither sudoers_cmnd nor user_cmnd are relative to runchroot.
477
     */
478
0
    if (cmnd[0] != '/' || regex_matches(sudoers_cmnd, cmnd) != ALLOW) {
479
  /* No match, retry using the canonicalized path (if possible). */
480
0
  if (ctx->user.cmnd_dir == NULL)
481
0
      debug_return_int(DENY);
482
0
  len = snprintf(buf, sizeof(buf), "%s/%s", ctx->user.cmnd_dir,
483
0
      ctx->user.cmnd_base);
484
0
  if (len < 0 || len >= ssizeof(buf))
485
0
      debug_return_int(DENY);
486
0
  cmnd = buf;
487
0
  if (regex_matches(sudoers_cmnd, cmnd) != ALLOW)
488
0
      debug_return_int(DENY);
489
0
    }
490
491
0
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
492
  /* Open the file for fdexec or for digest matching. */
493
0
  if (!open_cmnd(cmnd, runchroot, digests, &fd))
494
0
      goto bad;
495
#ifndef SUDOERS_NAME_MATCH
496
  if (!do_stat(fd, cmnd, runchroot, &sb))
497
      goto bad;
498
#endif
499
  /* Check digest of cmnd since sudoers_cmnd is a pattern. */
500
0
  if (digest_matches(fd, cmnd, runchroot, digests) != ALLOW)
501
0
      goto bad;
502
0
  set_cmnd_fd(ctx, fd);
503
504
  /* No need to set ctx->runas.cmnd since cmnd matches sudoers_cmnd */
505
0
  debug_return_int(ALLOW);
506
0
bad:
507
0
  if (fd != -1)
508
0
      close(fd);
509
0
    }
510
0
    debug_return_int(DENY);
511
0
}
512
513
#ifndef SUDOERS_NAME_MATCH
514
static int
515
command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd,
516
    const char *sudoers_args, const char *runchroot,
517
    const struct command_digest_list *digests)
518
{
519
    struct stat sudoers_stat;
520
    bool bad_digest = false;
521
    char **ap, *base, *cp;
522
    char pathbuf[PATH_MAX];
523
    int fd = -1;
524
    size_t dlen, chrootlen = 0;
525
    glob_t gl;
526
    debug_decl(command_matches_glob, SUDOERS_DEBUG_MATCH);
527
528
    /* Make sudoers_cmnd relative to the new root, if any. */
529
    if (runchroot != NULL) {
530
  /* XXX - handle symlinks and '..' in path outside chroot */
531
  const int len =
532
      snprintf(pathbuf, sizeof(pathbuf), "%s%s", runchroot, sudoers_cmnd);
533
  if (len >= ssizeof(pathbuf)) {
534
      errno = ENAMETOOLONG;
535
      sudo_warn("%s%s", runchroot, sudoers_cmnd);
536
      debug_return_int(DENY);
537
  }
538
  sudoers_cmnd = pathbuf;
539
  chrootlen = strlen(runchroot);
540
    }
541
542
    /*
543
     * First check to see if we can avoid the call to glob(3).
544
     * Short circuit if there are no meta chars in the command itself
545
     * and ctx->user.cmnd_base and basename(sudoers_cmnd) don't match.
546
     */
547
    dlen = strlen(sudoers_cmnd);
548
    if (sudoers_cmnd[dlen - 1] != '/') {
549
  base = sudo_basename(sudoers_cmnd);
550
  if (!has_meta(base) && strcmp(ctx->user.cmnd_base, base) != 0)
551
      debug_return_int(DENY);
552
    }
553
554
    /*
555
     * Return ALLOW if we find a match in the glob(3) results AND
556
     *  a) there are no args in sudoers OR
557
     *  b) there are no args on command line and none required by sudoers OR
558
     *  c) there are args in sudoers and on command line and they match
559
     * else return DENY.
560
     */
561
    if (glob(sudoers_cmnd, GLOB_NOSORT, NULL, &gl) != 0 || gl.gl_pathc == 0) {
562
  globfree(&gl);
563
  debug_return_int(DENY);
564
    }
565
566
    /* If ctx->user.cmnd is fully-qualified, check for an exact match. */
567
    if (ctx->user.cmnd[0] == '/') {
568
  for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
569
      if (fd != -1) {
570
    close(fd);
571
    fd = -1;
572
      }
573
      /* Remove the runchroot, if any. */
574
      cp += chrootlen;
575
576
      if (strcmp(cp, ctx->user.cmnd) != 0)
577
    continue;
578
      /* Open the file for fdexec or for digest matching. */
579
      if (!open_cmnd(cp, runchroot, digests, &fd))
580
    continue;
581
      if (!do_stat(fd, cp, runchroot, &sudoers_stat))
582
    continue;
583
      if (ctx->user.cmnd_stat == NULL ||
584
    (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
585
    ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
586
    /* There could be multiple matches, check digest early. */
587
    if (digest_matches(fd, cp, runchroot, digests) != ALLOW) {
588
        bad_digest = true;
589
        continue;
590
    }
591
    free(ctx->runas.cmnd);
592
    if ((ctx->runas.cmnd = strdup(cp)) == NULL) {
593
        sudo_warnx(U_("%s: %s"), __func__,
594
      U_("unable to allocate memory"));
595
        cp = NULL;    /* fail closed */
596
    }
597
      } else {
598
    /* Paths match, but st_dev and st_ino are different. */
599
    cp = NULL;    /* fail closed */
600
      }
601
      goto done;
602
  }
603
    }
604
    /* No exact match, compare basename, cmnd_dir, st_dev and st_ino. */
605
    if (!bad_digest) {
606
  for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
607
      if (fd != -1) {
608
    close(fd);
609
    fd = -1;
610
      }
611
      /* Remove the runchroot, if any. */
612
      cp += chrootlen;
613
614
      /* If it ends in '/' it is a directory spec. */
615
      dlen = strlen(cp);
616
      if (cp[dlen - 1] == '/') {
617
    if (command_matches_dir(ctx, cp, dlen, runchroot, digests) == ALLOW) {
618
        globfree(&gl);
619
        debug_return_int(ALLOW);
620
    }
621
    continue;
622
      }
623
624
      /* Only proceed if ctx->user.cmnd_base and basename(cp) match */
625
      base = sudo_basename(cp);
626
      if (strcmp(ctx->user.cmnd_base, base) != 0)
627
    continue;
628
629
      /* Compare the canonicalized parent directories, if possible. */
630
      if (ctx->user.cmnd_dir != NULL) {
631
    char *slash = strrchr(cp, '/');
632
    if (slash != NULL) {
633
        char *resolved;
634
        *slash = '\0';
635
        resolved = canon_path(cp);
636
        *slash = '/';
637
        if (resolved != NULL) {
638
      /* Canonicalized directories must match. */
639
      int result = strcmp(resolved, ctx->user.cmnd_dir);
640
      canon_path_free(resolved);
641
      if (result != 0)
642
          continue;
643
        }
644
    }
645
      }
646
647
      /* Open the file for fdexec or for digest matching. */
648
      if (!open_cmnd(cp, runchroot, digests, &fd))
649
    continue;
650
      if (!do_stat(fd, cp, runchroot, &sudoers_stat))
651
    continue;
652
      if (ctx->user.cmnd_stat == NULL ||
653
    (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
654
    ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
655
    if (digest_matches(fd, cp, runchroot, digests) != ALLOW)
656
        continue;
657
    free(ctx->runas.cmnd);
658
    if ((ctx->runas.cmnd = strdup(cp)) == NULL) {
659
        sudo_warnx(U_("%s: %s"), __func__,
660
      U_("unable to allocate memory"));
661
        cp = NULL;    /* fail closed */
662
    }
663
    goto done;
664
      }
665
  }
666
    }
667
done:
668
    globfree(&gl);
669
    if (cp != NULL) {
670
  if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
671
      /* ctx->runas.cmnd was set above. */
672
      set_cmnd_fd(ctx, fd);
673
      debug_return_int(ALLOW);
674
  }
675
    }
676
    if (fd != -1)
677
  close(fd);
678
    debug_return_int(DENY);
679
}
680
681
static int
682
command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd,
683
    const char *sudoers_args, const char *runchroot,
684
    const struct command_digest_list *digests)
685
{
686
    struct stat sudoers_stat;
687
    const char *base;
688
    size_t dlen;
689
    int fd = -1;
690
    debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH);
691
692
    /* If it ends in '/' it is a directory spec. */
693
    dlen = strlen(sudoers_cmnd);
694
    if (sudoers_cmnd[dlen - 1] == '/') {
695
  debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen,
696
      runchroot, digests));
697
    }
698
699
    /* Only proceed if ctx->user.cmnd_base and basename(sudoers_cmnd) match */
700
    base = sudo_basename(sudoers_cmnd);
701
    if (strcmp(ctx->user.cmnd_base, base) != 0)
702
  debug_return_int(DENY);
703
704
    /* Compare the canonicalized parent directories, if possible. */
705
    if (ctx->user.cmnd_dir != NULL) {
706
  const char *slash = strrchr(sudoers_cmnd, '/');
707
  if (slash != NULL) {
708
      char sudoers_cmnd_dir[PATH_MAX], *resolved;
709
      const size_t len = (size_t)(slash - sudoers_cmnd);
710
      if (len >= sizeof(sudoers_cmnd_dir))
711
    goto bad;
712
      if (len != 0)
713
    memcpy(sudoers_cmnd_dir, sudoers_cmnd, len);
714
      sudoers_cmnd_dir[len] = '\0';
715
      resolved = canon_path(sudoers_cmnd_dir);
716
      if (resolved != NULL) {
717
    if (strcmp(resolved, ctx->user.cmnd_dir) != 0) {
718
        canon_path_free(resolved);
719
        goto bad;
720
    }
721
    canon_path_free(resolved);
722
      }
723
  }
724
    }
725
726
    /* Open the file for fdexec or for digest matching. */
727
    if (!open_cmnd(sudoers_cmnd, runchroot, digests, &fd))
728
  goto bad;
729
730
    /*
731
     * Return true if command matches AND
732
     *  a) there are no args in sudoers OR
733
     *  b) there are no args on command line and none req by sudoers OR
734
     *  c) there are args in sudoers and on command line and they match
735
     *  d) there is a digest and it matches
736
     */
737
    if (ctx->user.cmnd_stat != NULL && do_stat(fd, sudoers_cmnd, runchroot, &sudoers_stat)) {
738
  if (ctx->user.cmnd_stat->st_dev != sudoers_stat.st_dev ||
739
      ctx->user.cmnd_stat->st_ino != sudoers_stat.st_ino)
740
      goto bad;
741
    } else {
742
  /* Either user or sudoers command does not exist, match by name. */
743
  if (strcmp(ctx->user.cmnd, sudoers_cmnd) != 0)
744
      goto bad;
745
    }
746
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) != ALLOW)
747
  goto bad;
748
    if (digest_matches(fd, sudoers_cmnd, runchroot, digests) != ALLOW) {
749
  /* XXX - log functions not available but we should log very loudly */
750
  goto bad;
751
    }
752
    free(ctx->runas.cmnd);
753
    if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) {
754
  sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
755
  goto bad;
756
    }
757
    set_cmnd_fd(ctx, fd);
758
    debug_return_int(ALLOW);
759
bad:
760
    if (fd != -1)
761
  close(fd);
762
    debug_return_int(DENY);
763
}
764
#else /* SUDOERS_NAME_MATCH */
765
static int
766
command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd,
767
    const char *sudoers_args, const char *runchroot,
768
    const struct command_digest_list *digests)
769
0
{
770
0
    return command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args, runchroot,
771
0
  digests);
772
0
}
773
774
static int
775
command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd,
776
    const char *sudoers_args, const char *runchroot,
777
    const struct command_digest_list *digests)
778
0
{
779
0
    size_t dlen;
780
0
    int fd = -1;
781
0
    debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH);
782
783
    /* If it ends in '/' it is a directory spec. */
784
0
    dlen = strlen(sudoers_cmnd);
785
0
    if (sudoers_cmnd[dlen - 1] == '/') {
786
0
  debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen, runchroot,
787
0
      digests));
788
0
    }
789
790
0
    if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0) {
791
0
  if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
792
      /* Open the file for fdexec or for digest matching. */
793
0
      if (!open_cmnd(ctx->user.cmnd, runchroot, digests, &fd))
794
0
    goto bad;
795
0
      if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
796
0
    goto bad;
797
798
      /* Successful match. */
799
0
      free(ctx->runas.cmnd);
800
0
      if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) {
801
0
    sudo_warnx(U_("%s: %s"), __func__,
802
0
        U_("unable to allocate memory"));
803
0
    goto bad;
804
0
      }
805
0
      set_cmnd_fd(ctx, fd);
806
0
      debug_return_int(ALLOW);
807
0
  }
808
0
    }
809
0
bad:
810
0
    if (fd != -1)
811
0
  close(fd);
812
0
    debug_return_int(DENY);
813
0
}
814
#endif /* SUDOERS_NAME_MATCH */
815
816
/*
817
 * If path doesn't end in /, return ALLOW iff cmnd & path name the same inode;
818
 * otherwise, return ALLOW if ctx->user.cmnd names one of the inodes in path.
819
 * Returns DENY on failure.
820
 */
821
int
822
command_matches(struct sudoers_context *ctx, const char *sudoers_cmnd,
823
    const char *sudoers_args, const char *runchroot, struct cmnd_info *info,
824
    const struct command_digest_list *digests)
825
57
{
826
57
    char *saved_user_cmnd = NULL;
827
57
    struct stat saved_user_stat;
828
57
    int ret = DENY;
829
57
    debug_decl(command_matches, SUDOERS_DEBUG_MATCH);
830
831
57
    if (ctx->runas.chroot != NULL) {
832
0
  if (runchroot != NULL && strcmp(runchroot, "*") != 0 &&
833
0
    strcmp(runchroot, ctx->runas.chroot) != 0) {
834
      /* CHROOT mismatch */
835
0
      goto done;
836
0
  }
837
  /* User-specified runchroot (cmnd_stat already set appropriately). */
838
0
  runchroot = ctx->runas.chroot;
839
57
    } else if (runchroot == NULL) {
840
  /* No rule-specific runchroot, use global (cmnd_stat already set). */
841
55
  if (def_runchroot != NULL && strcmp(def_runchroot, "*") != '\0')
842
0
      runchroot = def_runchroot;
843
55
    } else {
844
  /* Rule-specific runchroot, must reset cmnd and cmnd_stat. */
845
2
  int status;
846
847
  /* Save old ctx->user.cmnd first, set_cmnd_path() will free it. */
848
2
  saved_user_cmnd = ctx->user.cmnd;
849
2
  ctx->user.cmnd = NULL;
850
2
  if (ctx->user.cmnd_stat != NULL)
851
0
      saved_user_stat = *ctx->user.cmnd_stat;
852
2
  status = set_cmnd_path(ctx, runchroot);
853
2
  if (status != FOUND) {
854
0
      ctx->user.cmnd = saved_user_cmnd;
855
0
      saved_user_cmnd = NULL;
856
0
  }
857
2
  if (info != NULL)
858
2
      info->status = status;
859
2
    }
860
861
57
    if (sudoers_cmnd == NULL) {
862
36
  sudoers_cmnd = "ALL";
863
36
  ret = command_matches_all(ctx, runchroot, digests);
864
36
  goto done;
865
36
    }
866
867
    /* Check for regular expressions first. */
868
21
    if (sudoers_cmnd[0] == '^') {
869
0
  ret = command_matches_regex(ctx, sudoers_cmnd, sudoers_args, runchroot,
870
0
      digests);
871
0
  goto done;
872
0
    }
873
874
    /* Check for pseudo-commands */
875
21
    if (sudoers_cmnd[0] != '/') {
876
  /*
877
   * Return true if sudoers_cmnd and cmnd match a pseudo-command AND
878
   *  a) there are no args in sudoers OR
879
   *  b) there are no args on command line and none req by sudoers OR
880
   *  c) there are args in sudoers and on command line and they match
881
   */
882
2
  if (strcmp(sudoers_cmnd, "list") == 0 ||
883
2
    strcmp(sudoers_cmnd, "sudoedit") == 0) {
884
2
      if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0 &&
885
0
        command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
886
    /* No need to set ctx->user.cmnd since cmnd == sudoers_cmnd */
887
0
    ret = ALLOW;
888
0
      }
889
2
  }
890
2
  goto done;
891
2
    }
892
893
19
    if (has_meta(sudoers_cmnd)) {
894
  /*
895
   * If sudoers_cmnd has meta characters in it, we need to
896
   * use glob(3) and/or fnmatch(3) to do the matching.
897
   */
898
19
  if (def_fast_glob) {
899
19
      ret = command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args,
900
19
    runchroot, digests);
901
19
  } else {
902
0
      ret = command_matches_glob(ctx, sudoers_cmnd, sudoers_args,
903
0
    runchroot, digests);
904
0
  }
905
19
    } else {
906
0
  ret = command_matches_normal(ctx, sudoers_cmnd, sudoers_args,
907
0
      runchroot, digests);
908
0
    }
909
57
done:
910
    /* Restore ctx->user.cmnd and ctx->user.cmnd_stat. */
911
57
    if (saved_user_cmnd != NULL) {
912
2
  if (info != NULL) {
913
2
      free(info->cmnd_path);
914
2
      info->cmnd_path = ctx->user.cmnd;
915
2
      if (ctx->user.cmnd_stat != NULL)
916
0
    info->cmnd_stat = *ctx->user.cmnd_stat;
917
2
  } else {
918
0
      free(ctx->user.cmnd);
919
0
  }
920
2
  ctx->user.cmnd = saved_user_cmnd;
921
2
  if (ctx->user.cmnd_stat != NULL)
922
0
      *ctx->user.cmnd_stat = saved_user_stat;
923
2
    }
924
57
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
925
57
  "user command \"%s%s%s\" matches sudoers command \"%s%s%s\"%s%s: %s",
926
57
  ctx->user.cmnd, ctx->user.cmnd_args ? " " : "",
927
57
  ctx->user.cmnd_args ? ctx->user.cmnd_args : "", sudoers_cmnd,
928
57
  sudoers_args ? " " : "", sudoers_args ? sudoers_args : "",
929
57
  runchroot ? ", chroot " : "", runchroot ? runchroot : "",
930
57
  ret == ALLOW ? "ALLOW" : "DENY");
931
57
    debug_return_int(ret);
932
57
}