Coverage Report

Created: 2026-08-13 06:32

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