Coverage Report

Created: 2026-05-30 06:41

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
10
{
83
10
    const char *args = ctx->user.cmnd_args ? ctx->user.cmnd_args : "";
84
10
    int flags = 0;
85
10
    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
10
    if (sudoers_args == NULL)
92
10
  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
10
{
171
10
    int fd;
172
10
    char pathbuf[PATH_MAX];
173
10
    debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH);
174
175
    /* Only open the file for fdexec or for digest matching. */
176
10
    if (def_fdexec != always && TAILQ_EMPTY(digests))
177
4
  debug_return_bool(true);
178
179
    /* Make path relative to the new root, if any. */
180
6
    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
6
    fd = open(path, O_RDONLY|O_NONBLOCK);
192
6
# ifdef O_EXEC
193
6
    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
6
# endif
200
6
    if (fd == -1)
201
0
  debug_return_bool(false);
202
203
6
    (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
204
6
    *fdp = fd;
205
6
    debug_return_bool(true);
206
6
}
207
208
static void
209
set_cmnd_fd(struct sudoers_context *ctx, int fd)
210
4
{
211
4
    debug_decl(set_cmnd_fd, SUDOERS_DEBUG_MATCH);
212
213
4
    if (ctx->runas.execfd != -1)
214
1
  close(ctx->runas.execfd);
215
216
4
    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
4
    ctx->runas.execfd = fd;
244
245
4
    debug_return;
246
4
}
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
4
{
401
4
    const char *cmnd = ctx->user.cmnd;
402
4
    char buf[PATH_MAX];
403
4
    int len, fd = -1;
404
#ifndef SUDOERS_NAME_MATCH
405
    struct stat sb;
406
#endif
407
4
    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
4
    if (cmnd[0] != '/' || sudo_contains_dot_dot(cmnd) ||
421
4
      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
4
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
435
  /* Open the file for fdexec or for digest matching. */
436
4
  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
4
  if (digest_matches(fd, cmnd, runchroot, digests) != ALLOW)
444
0
      goto bad;
445
4
  set_cmnd_fd(ctx, fd);
446
447
  /* No need to set ctx->runas.cmnd since cmnd matches sudoers_cmnd */
448
4
  debug_return_int(ALLOW);
449
0
bad:
450
0
  if (fd != -1)
451
0
      close(fd);
452
0
    }
453
0
    debug_return_int(DENY);
454
0
}
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] != '/' || sudo_contains_dot_dot(cmnd) ||
479
0
      regex_matches(sudoers_cmnd, cmnd) != ALLOW) {
480
  /* No match, retry using the canonicalized path (if possible). */
481
0
  if (ctx->user.cmnd_dir == NULL)
482
0
      debug_return_int(DENY);
483
0
  len = snprintf(buf, sizeof(buf), "%s/%s", ctx->user.cmnd_dir,
484
0
      ctx->user.cmnd_base);
485
0
  if (len < 0 || len >= ssizeof(buf))
486
0
      debug_return_int(DENY);
487
0
  cmnd = buf;
488
0
  if (regex_matches(sudoers_cmnd, cmnd) != ALLOW)
489
0
      debug_return_int(DENY);
490
0
    }
491
492
0
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
493
  /* Open the file for fdexec or for digest matching. */
494
0
  if (!open_cmnd(cmnd, runchroot, digests, &fd))
495
0
      goto bad;
496
#ifndef SUDOERS_NAME_MATCH
497
  if (!do_stat(fd, cmnd, runchroot, &sb))
498
      goto bad;
499
#endif
500
  /* Check digest of cmnd since sudoers_cmnd is a pattern. */
501
0
  if (digest_matches(fd, cmnd, runchroot, digests) != ALLOW)
502
0
      goto bad;
503
0
  set_cmnd_fd(ctx, fd);
504
505
  /* No need to set ctx->runas.cmnd since cmnd matches sudoers_cmnd */
506
0
  debug_return_int(ALLOW);
507
0
bad:
508
0
  if (fd != -1)
509
0
      close(fd);
510
0
    }
511
0
    debug_return_int(DENY);
512
0
}
513
514
#ifndef SUDOERS_NAME_MATCH
515
static int
516
command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd,
517
    const char *sudoers_args, const char *runchroot,
518
    const struct command_digest_list *digests)
519
{
520
    struct stat sudoers_stat;
521
    bool bad_digest = false;
522
    char **ap, *base, *cp;
523
    char pathbuf[PATH_MAX];
524
    int fd = -1;
525
    size_t dlen, chrootlen = 0;
526
    glob_t gl;
527
    debug_decl(command_matches_glob, SUDOERS_DEBUG_MATCH);
528
529
    /* Make sudoers_cmnd relative to the new root, if any. */
530
    if (runchroot != NULL) {
531
  /* XXX - handle symlinks and '..' in path outside chroot */
532
  const int len =
533
      snprintf(pathbuf, sizeof(pathbuf), "%s%s", runchroot, sudoers_cmnd);
534
  if (len >= ssizeof(pathbuf)) {
535
      errno = ENAMETOOLONG;
536
      sudo_warn("%s%s", runchroot, sudoers_cmnd);
537
      debug_return_int(DENY);
538
  }
539
  sudoers_cmnd = pathbuf;
540
  chrootlen = strlen(runchroot);
541
    }
542
543
    /*
544
     * First check to see if we can avoid the call to glob(3).
545
     * Short circuit if there are no meta chars in the command itself
546
     * and ctx->user.cmnd_base and basename(sudoers_cmnd) don't match.
547
     */
548
    dlen = strlen(sudoers_cmnd);
549
    if (sudoers_cmnd[dlen - 1] != '/') {
550
  base = sudo_basename(sudoers_cmnd);
551
  if (!has_meta(base) && strcmp(ctx->user.cmnd_base, base) != 0)
552
      debug_return_int(DENY);
553
    }
554
555
    /*
556
     * Return ALLOW if we find a match in the glob(3) results AND
557
     *  a) there are no args in sudoers OR
558
     *  b) there are no args on command line and none required by sudoers OR
559
     *  c) there are args in sudoers and on command line and they match
560
     * else return DENY.
561
     */
562
    if (glob(sudoers_cmnd, GLOB_NOSORT, NULL, &gl) != 0 || gl.gl_pathc == 0) {
563
  globfree(&gl);
564
  debug_return_int(DENY);
565
    }
566
567
    /* If ctx->user.cmnd is fully-qualified, check for an exact match. */
568
    if (ctx->user.cmnd[0] == '/') {
569
  for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
570
      if (fd != -1) {
571
    close(fd);
572
    fd = -1;
573
      }
574
      /* Remove the runchroot, if any. */
575
      cp += chrootlen;
576
577
      if (strcmp(cp, ctx->user.cmnd) != 0)
578
    continue;
579
      /* Open the file for fdexec or for digest matching. */
580
      if (!open_cmnd(cp, runchroot, digests, &fd))
581
    continue;
582
      if (!do_stat(fd, cp, runchroot, &sudoers_stat))
583
    continue;
584
      if (ctx->user.cmnd_stat == NULL ||
585
    (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
586
    ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
587
    /* There could be multiple matches, check digest early. */
588
    if (digest_matches(fd, cp, runchroot, digests) != ALLOW) {
589
        bad_digest = true;
590
        continue;
591
    }
592
    free(ctx->runas.cmnd);
593
    if ((ctx->runas.cmnd = strdup(cp)) == NULL) {
594
        sudo_warnx(U_("%s: %s"), __func__,
595
      U_("unable to allocate memory"));
596
        cp = NULL;    /* fail closed */
597
    }
598
      } else {
599
    /* Paths match, but st_dev and st_ino are different. */
600
    cp = NULL;    /* fail closed */
601
      }
602
      goto done;
603
  }
604
    }
605
    /* No exact match, compare basename, cmnd_dir, st_dev and st_ino. */
606
    if (!bad_digest) {
607
  for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
608
      if (fd != -1) {
609
    close(fd);
610
    fd = -1;
611
      }
612
      /* Remove the runchroot, if any. */
613
      cp += chrootlen;
614
615
      /* If it ends in '/' it is a directory spec. */
616
      dlen = strlen(cp);
617
      if (cp[dlen - 1] == '/') {
618
    if (command_matches_dir(ctx, cp, dlen, runchroot, digests) == ALLOW) {
619
        globfree(&gl);
620
        debug_return_int(ALLOW);
621
    }
622
    continue;
623
      }
624
625
      /* Only proceed if ctx->user.cmnd_base and basename(cp) match */
626
      base = sudo_basename(cp);
627
      if (strcmp(ctx->user.cmnd_base, base) != 0)
628
    continue;
629
630
      /* Compare the canonicalized parent directories, if possible. */
631
      if (ctx->user.cmnd_dir != NULL) {
632
    char *slash = strrchr(cp, '/');
633
    if (slash != NULL) {
634
        char *resolved;
635
        *slash = '\0';
636
        resolved = canon_path(cp);
637
        *slash = '/';
638
        if (resolved != NULL) {
639
      /* Canonicalized directories must match. */
640
      int result = strcmp(resolved, ctx->user.cmnd_dir);
641
      canon_path_free(resolved);
642
      if (result != 0)
643
          continue;
644
        }
645
    }
646
      }
647
648
      /* Open the file for fdexec or for digest matching. */
649
      if (!open_cmnd(cp, runchroot, digests, &fd))
650
    continue;
651
      if (!do_stat(fd, cp, runchroot, &sudoers_stat))
652
    continue;
653
      if (ctx->user.cmnd_stat == NULL ||
654
    (ctx->user.cmnd_stat->st_dev == sudoers_stat.st_dev &&
655
    ctx->user.cmnd_stat->st_ino == sudoers_stat.st_ino)) {
656
    if (digest_matches(fd, cp, runchroot, digests) != ALLOW)
657
        continue;
658
    free(ctx->runas.cmnd);
659
    if ((ctx->runas.cmnd = strdup(cp)) == NULL) {
660
        sudo_warnx(U_("%s: %s"), __func__,
661
      U_("unable to allocate memory"));
662
        cp = NULL;    /* fail closed */
663
    }
664
    goto done;
665
      }
666
  }
667
    }
668
done:
669
    globfree(&gl);
670
    if (cp != NULL) {
671
  if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
672
      /* ctx->runas.cmnd was set above. */
673
      set_cmnd_fd(ctx, fd);
674
      debug_return_int(ALLOW);
675
  }
676
    }
677
    if (fd != -1)
678
  close(fd);
679
    debug_return_int(DENY);
680
}
681
682
static int
683
command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd,
684
    const char *sudoers_args, const char *runchroot,
685
    const struct command_digest_list *digests)
686
{
687
    struct stat sudoers_stat;
688
    const char *base;
689
    size_t dlen;
690
    int fd = -1;
691
    debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH);
692
693
    /* If it ends in '/' it is a directory spec. */
694
    dlen = strlen(sudoers_cmnd);
695
    if (sudoers_cmnd[dlen - 1] == '/') {
696
  debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen,
697
      runchroot, digests));
698
    }
699
700
    /* Only proceed if ctx->user.cmnd_base and basename(sudoers_cmnd) match */
701
    base = sudo_basename(sudoers_cmnd);
702
    if (strcmp(ctx->user.cmnd_base, base) != 0)
703
  debug_return_int(DENY);
704
705
    /* Compare the canonicalized parent directories, if possible. */
706
    if (ctx->user.cmnd_dir != NULL) {
707
  const char *slash = strrchr(sudoers_cmnd, '/');
708
  if (slash != NULL) {
709
      char sudoers_cmnd_dir[PATH_MAX], *resolved;
710
      const size_t len = (size_t)(slash - sudoers_cmnd);
711
      if (len >= sizeof(sudoers_cmnd_dir))
712
    goto bad;
713
      if (len != 0)
714
    memcpy(sudoers_cmnd_dir, sudoers_cmnd, len);
715
      sudoers_cmnd_dir[len] = '\0';
716
      resolved = canon_path(sudoers_cmnd_dir);
717
      if (resolved != NULL) {
718
    if (strcmp(resolved, ctx->user.cmnd_dir) != 0) {
719
        canon_path_free(resolved);
720
        goto bad;
721
    }
722
    canon_path_free(resolved);
723
      }
724
  }
725
    }
726
727
    /* Open the file for fdexec or for digest matching. */
728
    if (!open_cmnd(sudoers_cmnd, runchroot, digests, &fd))
729
  goto bad;
730
731
    /*
732
     * Return true if command matches AND
733
     *  a) there are no args in sudoers OR
734
     *  b) there are no args on command line and none req by sudoers OR
735
     *  c) there are args in sudoers and on command line and they match
736
     *  d) there is a digest and it matches
737
     */
738
    if (ctx->user.cmnd_stat != NULL && do_stat(fd, sudoers_cmnd, runchroot, &sudoers_stat)) {
739
  if (ctx->user.cmnd_stat->st_dev != sudoers_stat.st_dev ||
740
      ctx->user.cmnd_stat->st_ino != sudoers_stat.st_ino)
741
      goto bad;
742
    } else {
743
  /* Either user or sudoers command does not exist, match by name. */
744
  if (strcmp(ctx->user.cmnd, sudoers_cmnd) != 0)
745
      goto bad;
746
    }
747
    if (command_args_match(ctx, sudoers_cmnd, sudoers_args) != ALLOW)
748
  goto bad;
749
    if (digest_matches(fd, sudoers_cmnd, runchroot, digests) != ALLOW) {
750
  /* XXX - log functions not available but we should log very loudly */
751
  goto bad;
752
    }
753
    free(ctx->runas.cmnd);
754
    if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) {
755
  sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
756
  goto bad;
757
    }
758
    set_cmnd_fd(ctx, fd);
759
    debug_return_int(ALLOW);
760
bad:
761
    if (fd != -1)
762
  close(fd);
763
    debug_return_int(DENY);
764
}
765
#else /* SUDOERS_NAME_MATCH */
766
static int
767
command_matches_glob(struct sudoers_context *ctx, const char *sudoers_cmnd,
768
    const char *sudoers_args, const char *runchroot,
769
    const struct command_digest_list *digests)
770
0
{
771
0
    return command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args, runchroot,
772
0
  digests);
773
0
}
774
775
static int
776
command_matches_normal(struct sudoers_context *ctx, const char *sudoers_cmnd,
777
    const char *sudoers_args, const char *runchroot,
778
    const struct command_digest_list *digests)
779
6
{
780
6
    size_t dlen;
781
6
    int fd = -1;
782
6
    debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH);
783
784
    /* If it ends in '/' it is a directory spec. */
785
6
    dlen = strlen(sudoers_cmnd);
786
6
    if (sudoers_cmnd[dlen - 1] == '/') {
787
0
  debug_return_int(command_matches_dir(ctx, sudoers_cmnd, dlen, runchroot,
788
0
      digests));
789
0
    }
790
791
6
    if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0) {
792
6
  if (command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
793
      /* Open the file for fdexec or for digest matching. */
794
6
      if (!open_cmnd(ctx->user.cmnd, runchroot, digests, &fd))
795
0
    goto bad;
796
6
      if (digest_matches(fd, ctx->user.cmnd, runchroot, digests) != ALLOW)
797
6
    goto bad;
798
799
      /* Successful match. */
800
0
      free(ctx->runas.cmnd);
801
0
      if ((ctx->runas.cmnd = strdup(sudoers_cmnd)) == NULL) {
802
0
    sudo_warnx(U_("%s: %s"), __func__,
803
0
        U_("unable to allocate memory"));
804
0
    goto bad;
805
0
      }
806
0
      set_cmnd_fd(ctx, fd);
807
0
      debug_return_int(ALLOW);
808
0
  }
809
6
    }
810
6
bad:
811
6
    if (fd != -1)
812
6
  close(fd);
813
6
    debug_return_int(DENY);
814
6
}
815
#endif /* SUDOERS_NAME_MATCH */
816
817
/*
818
 * If path doesn't end in /, return ALLOW iff cmnd & path name the same inode;
819
 * otherwise, return ALLOW if ctx->user.cmnd names one of the inodes in path.
820
 * Returns DENY on failure.
821
 */
822
int
823
command_matches(struct sudoers_context *ctx, const char *sudoers_cmnd,
824
    const char *sudoers_args, const char *runchroot, struct cmnd_info *info,
825
    const struct command_digest_list *digests)
826
10
{
827
10
    char *saved_user_cmnd = NULL;
828
10
    struct stat saved_user_stat;
829
10
    int ret = DENY;
830
10
    debug_decl(command_matches, SUDOERS_DEBUG_MATCH);
831
832
10
    if (ctx->runas.chroot != NULL) {
833
0
  if (runchroot != NULL && strcmp(runchroot, "*") != 0 &&
834
0
    strcmp(runchroot, ctx->runas.chroot) != 0) {
835
      /* CHROOT mismatch */
836
0
      goto done;
837
0
  }
838
  /* User-specified runchroot (cmnd_stat already set appropriately). */
839
0
  runchroot = ctx->runas.chroot;
840
10
    } else if (runchroot == NULL) {
841
  /* No rule-specific runchroot, use global (cmnd_stat already set). */
842
10
  if (def_runchroot != NULL && strcmp(def_runchroot, "*") != '\0')
843
0
      runchroot = def_runchroot;
844
10
    } else {
845
  /* Rule-specific runchroot, must reset cmnd and cmnd_stat. */
846
0
  int status;
847
848
  /* Save old ctx->user.cmnd first, set_cmnd_path() will free it. */
849
0
  saved_user_cmnd = ctx->user.cmnd;
850
0
  ctx->user.cmnd = NULL;
851
0
  if (ctx->user.cmnd_stat != NULL)
852
0
      saved_user_stat = *ctx->user.cmnd_stat;
853
0
  status = set_cmnd_path(ctx, runchroot);
854
0
  if (status != FOUND) {
855
0
      ctx->user.cmnd = saved_user_cmnd;
856
0
      saved_user_cmnd = NULL;
857
0
  }
858
0
  if (info != NULL)
859
0
      info->status = status;
860
0
    }
861
862
10
    if (sudoers_cmnd == NULL) {
863
0
  sudoers_cmnd = "ALL";
864
0
  ret = command_matches_all(ctx, runchroot, digests);
865
0
  goto done;
866
0
    }
867
868
    /* Check for regular expressions first. */
869
10
    if (sudoers_cmnd[0] == '^') {
870
0
  ret = command_matches_regex(ctx, sudoers_cmnd, sudoers_args, runchroot,
871
0
      digests);
872
0
  goto done;
873
0
    }
874
875
    /* Check for pseudo-commands */
876
10
    if (sudoers_cmnd[0] != '/') {
877
  /*
878
   * Return true if sudoers_cmnd and cmnd match a pseudo-command AND
879
   *  a) there are no args in sudoers OR
880
   *  b) there are no args on command line and none req by sudoers OR
881
   *  c) there are args in sudoers and on command line and they match
882
   */
883
0
  if (strcmp(sudoers_cmnd, "list") == 0 ||
884
0
    strcmp(sudoers_cmnd, "sudoedit") == 0) {
885
0
      if (strcmp(ctx->user.cmnd, sudoers_cmnd) == 0 &&
886
0
        command_args_match(ctx, sudoers_cmnd, sudoers_args) == ALLOW) {
887
    /* No need to set ctx->user.cmnd since cmnd == sudoers_cmnd */
888
0
    ret = ALLOW;
889
0
      }
890
0
  }
891
0
  goto done;
892
0
    }
893
894
10
    if (has_meta(sudoers_cmnd)) {
895
  /*
896
   * If sudoers_cmnd has meta characters in it, we need to
897
   * use glob(3) and/or fnmatch(3) to do the matching.
898
   */
899
4
  if (def_fast_glob) {
900
4
      ret = command_matches_fnmatch(ctx, sudoers_cmnd, sudoers_args,
901
4
    runchroot, digests);
902
4
  } else {
903
0
      ret = command_matches_glob(ctx, sudoers_cmnd, sudoers_args,
904
0
    runchroot, digests);
905
0
  }
906
6
    } else {
907
6
  ret = command_matches_normal(ctx, sudoers_cmnd, sudoers_args,
908
6
      runchroot, digests);
909
6
    }
910
10
done:
911
    /* Restore ctx->user.cmnd and ctx->user.cmnd_stat. */
912
10
    if (saved_user_cmnd != NULL) {
913
0
  if (info != NULL) {
914
0
      free(info->cmnd_path);
915
0
      info->cmnd_path = ctx->user.cmnd;
916
0
      if (ctx->user.cmnd_stat != NULL)
917
0
    info->cmnd_stat = *ctx->user.cmnd_stat;
918
0
  } else {
919
0
      free(ctx->user.cmnd);
920
0
  }
921
0
  ctx->user.cmnd = saved_user_cmnd;
922
0
  if (ctx->user.cmnd_stat != NULL)
923
0
      *ctx->user.cmnd_stat = saved_user_stat;
924
0
    }
925
10
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
926
10
  "user command \"%s%s%s\" matches sudoers command \"%s%s%s\"%s%s: %s",
927
10
  ctx->user.cmnd, ctx->user.cmnd_args ? " " : "",
928
10
  ctx->user.cmnd_args ? ctx->user.cmnd_args : "", sudoers_cmnd,
929
10
  sudoers_args ? " " : "", sudoers_args ? sudoers_args : "",
930
10
  runchroot ? ", chroot " : "", runchroot ? runchroot : "",
931
10
  ret == ALLOW ? "ALLOW" : "DENY");
932
10
    debug_return_int(ret);
933
10
}