Coverage Report

Created: 2026-11-12 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/sudo/plugins/sudoers/rationalize.c
Line
Count
Source
1
/*
2
 * SPDX-License-Identifier: ISC
3
 *
4
 * Copyright (c) 2026 Todd C. Miller <Todd.Miller@sudo.ws>
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 */
18
19
#include <config.h>
20
21
#include <assert.h>
22
#include <string.h>
23
24
#include <sudoers.h>
25
26
/*
27
 * Rationalize a path by removing consecutive '/' and "/./" path elements.
28
 * Also removes "/../" elements if dotdot is true.
29
 */
30
char *
31
rationalize_path(char *path, bool dotdot)
32
3.72M
{
33
3.72M
    char *cp, *ep, *path_end;
34
3.72M
    debug_decl(rationalize_path, SUDOERS_DEBUG_UTIL);
35
36
3.72M
    path_end = path + strlen(path);
37
1.48G
    for (cp = path; *cp != '\0';) {
38
1.48G
  if (cp[0] == '/') {
39
      /* Collapse consecutive '/' characters. */
40
4.60M
      if (cp[1] == '/') {
41
509k
    for (ep = cp + 1; ep[1] == '/'; ep++)
42
192k
        continue;
43
44
    /* The size argument includes the terminating NUL byte. */
45
317k
    assert(ep <= path_end);
46
317k
    memmove(cp, ep, (size_t)(path_end - ep) + 1);
47
317k
    path_end -= ep - cp;
48
317k
    assert(*path_end == '\0');
49
317k
    continue;
50
317k
      }
51
52
      /* Remove "/./" in path or "/." at path_end. */
53
4.28M
      if (cp[1] == '.' && (cp[2] == '/' || cp[2] == '\0')) {
54
    /* /./foo -> /foo OR /foo/. -> /foo */
55
31.6k
    ep = cp + 2;
56
57
    /* The size argument includes the terminating NUL byte. */
58
31.6k
    assert(ep <= path_end);
59
31.6k
    memmove(cp, ep, (size_t)(path_end - ep) + 1);
60
31.6k
    path_end -= ep - cp;
61
62
    /* An absolute path must not collapse to the empty string. */
63
31.6k
    if (cp == path && *cp == '\0') {
64
        /* /. -> / */
65
4.15k
        path[0] = '/';
66
4.15k
        path[1] = '\0';
67
4.15k
        path_end = path + 1;
68
4.15k
    }
69
31.6k
    assert(*path_end == '\0');
70
31.6k
    continue;
71
31.6k
      }
72
73
      /* Remove "/../" in path or "/.." at path_end. */
74
4.25M
      if (dotdot && cp[1] == '.' && cp[2] == '.' &&
75
204k
        (cp[3] == '/' || cp[3] == '\0')) {
76
77
    /* Skip ep past "/../" or "/.." */
78
171k
    ep = cp + 3 + (cp[3] == '/');
79
80
    /* Set cp to the previous '/' if there is one. */
81
171k
    if (cp != path) {
82
47.4k
        char *saved_cp = cp;
83
47.4k
        ep = cp + 3 + (cp[3] == '/');
84
335k
        do {
85
335k
      cp--;
86
335k
        } while (cp > path && *cp != '/');
87
47.4k
        if (*cp != '/') {
88
      /* No previous '/', path doesn't start with '/'? */
89
22.8k
      cp = saved_cp;
90
22.8k
        }
91
47.4k
    }
92
93
    /* The size argument includes the terminating NUL byte. */
94
171k
    assert(ep <= path_end);
95
171k
    memmove(cp + 1, ep, (size_t)(path_end - ep) + 1);
96
171k
    path_end -= ep - (cp + 1);
97
171k
    assert(*path_end == '\0');
98
171k
    continue;
99
171k
      }
100
4.25M
  }
101
1.47G
  cp++;
102
1.47G
    }
103
3.72M
    debug_return_str(path);
104
3.72M
}