File: ShellUnquote.cc

package info (click to toggle)
j4-dmenu-desktop 3.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,180 kB
  • sloc: cpp: 6,282; python: 473; sh: 81; makefile: 8
file content (134 lines) | stat: -rw-r--r-- 4,099 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//
// This file is part of j4-dmenu-desktop.
//
// j4-dmenu-desktop is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// j4-dmenu-desktop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with j4-dmenu-desktop.  If not, see <http://www.gnu.org/licenses/>.
//

#include <catch2/catch_test_macros.hpp>

#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <errno.h>
#include <fmt/core.h>
#include <memory>
#include <signal.h>
#include <stdio.h>
#include <string>
#include <string_view>
#include <sys/types.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>

#include "Utilities.hh"

namespace ShellUnquote
{
static pid_t waitpid_nointerr(pid_t pid, int *wstatus, int options) {
    pid_t result;
    do {
        result = waitpid(pid, wstatus, options);
    } while (result == -1 && errno == EINTR);
    return result;
}

// Pass quoted string str to shell to apply shells unescaping rules and return
// the unescaped result.
std::string posix_shell_unqote(std::string_view str) {
    using namespace std::literals::chrono_literals;

    int pipefd[2];
    if (pipe(pipefd) == -1)
        SKIP("Couldn't pipe(): " << strerror(errno));

    pid_t pid;
    switch ((pid = fork())) {
    case -1:
        SKIP("Couldn't fork(): " << strerror(errno));
        break;
    case 0:
        std::string request = "printf %s " + std::string(str);

        close(pipefd[0]);
        if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
            fmt::print(stderr, "Couldn't dup2(): {}", strerror(errno));
            _exit(EXIT_FAILURE);
        }
        close(pipefd[1]);

        const char *argv[] = {"/bin/sh", "-c", request.c_str(), (char *)NULL};
        execv("/bin/sh", (char **)argv);
        _exit(EXIT_FAILURE);
    }

    close(pipefd[1]);

    std::shared_ptr<std::atomic_flag> process_terminated =
        std::make_shared<std::atomic_flag>();
    process_terminated->clear();

    std::thread timeout_terminator(
        [](pid_t pid, std::shared_ptr<std::atomic_flag> process_terminated) {
            std::this_thread::sleep_for(5s);
            if (!process_terminated->test_and_set()) {
                kill(pid, SIGTERM);
                std::this_thread::sleep_for(500ms);
                kill(pid, SIGKILL);
            }
        },
        pid, process_terminated);
    timeout_terminator.detach();

    int status;
    pid_t waitpid_result = waitpid_nointerr(pid, &status, 0);

    // <--
    // There could be a race condition between these lines of code. This is code
    // for unit tests, not for j4-dmenu-desktop, so I find this tolerable. The
    // probability of hitting this race condition is minuscule. If input string
    // is escaped well, no shell injection happened and the unit tests aren't
    // being run on a extremely slow computer, the race condition will never be
    // hit.
    // <--

    if (process_terminated->test_and_set()) {
        FAIL("Shell process timed out!");
    }

    if (waitpid_result == -1)
        SKIP("Couldn't waitpid(): " << strerror(errno));

    if (!WIFEXITED(status))
        FAIL("/bin/sh exited abnormally!");
    if (WEXITSTATUS(status) != 0)
        FAIL("/bin/sh exited with nonzero exit status! Exit status "
             << std::to_string(WEXITSTATUS(status)));

    std::string result;
    char buf[1024];
    ssize_t read_size;
    while ((read_size = readn(pipefd[0], buf, sizeof buf)) > 0)
        result.append(buf, read_size);

    close(pipefd[0]);

    // readn() from Utilities.hh handles EINTR well
    if (read_size == -1)
        SKIP("Couldn't read(): " << strerror(errno));

    return result;
}
}; // namespace ShellUnquote