Skip to content
API Documentation
BastionGuard™
SafeProcess.hpp
Go to the documentation of this file.
1#pragma once
2
3// Execute programs with an argv vector, never through a command interpreter.
4// Output and exit status are bounded/normalized; callbacks can stop a scan.
5#include <algorithm>
6#include <cerrno>
7#include <csignal>
8#include <functional>
9#include <map>
10#include <cstdlib>
11#include <sys/stat.h>
12#include <string>
13#include <vector>
14#include <spawn.h>
15#include <sys/wait.h>
16#include <unistd.h>
17
18extern char **environ;
19
20namespace bg_process {
21struct Result {
22 int exit_code = 127;
23 std::string output;
24 bool truncated = false;
25 bool started = false;
26 bool ok() const { return started && !truncated && exit_code == 0; }
27};
28
29// Resolve only executable names; do not interpret shell syntax or options.
30inline bool exists_on_path(const std::string& name) {
31 if (name.empty() || name.find('/') != std::string::npos || name == "." || name == "..")
32 return false;
33 const char* path = std::getenv("PATH");
34 if (!path) return false;
35 const std::string dirs(path);
36 std::size_t start = 0;
37 do {
38 const auto end = dirs.find(':', start);
39 const std::string dir = dirs.substr(start, end == std::string::npos ? end : end - start);
40 // Empty PATH components mean current directory: not trustworthy for security checks.
41 if (!dir.empty()) {
42 const std::string file = dir + "/" + name;
43 struct stat st{};
44 if (::stat(file.c_str(), &st) == 0 && S_ISREG(st.st_mode) &&
45 ::access(file.c_str(), X_OK) == 0) return true;
46 }
47 if (end == std::string::npos) break;
48 start = end + 1;
49 } while (true);
50 return false;
51}
52
53inline Result run(const std::vector<std::string>& args,
54 bool merge_stderr = false,
55 std::size_t limit = 8U * 1024U * 1024U,
56 const std::map<std::string, std::string>& environment = {},
57 const std::function<bool(const std::string&)>& on_line = {}) {
58 Result result;
59 if (args.empty() || args[0].empty()) return result;
60 for (const auto& arg : args) if (arg.find('\0') != std::string::npos) return result;
61
62 int fd[2];
63 if (::pipe(fd) != 0) return result;
64 posix_spawn_file_actions_t actions;
65 if (posix_spawn_file_actions_init(&actions) != 0) {
66 ::close(fd[0]); ::close(fd[1]); return result;
67 }
68 posix_spawn_file_actions_addclose(&actions, fd[0]);
69 posix_spawn_file_actions_adddup2(&actions, fd[1], STDOUT_FILENO);
70 if (merge_stderr) posix_spawn_file_actions_adddup2(&actions, fd[1], STDERR_FILENO);
71 posix_spawn_file_actions_addclose(&actions, fd[1]);
72
73 std::vector<char*> argv;
74 for (const auto& arg : args) argv.push_back(const_cast<char*>(arg.c_str()));
75 argv.push_back(nullptr);
76
77 // Apply only explicitly supplied overrides, without invoking /usr/bin/env or shell.
78 std::vector<std::string> env_storage;
79 std::vector<char*> envp;
80 if (!environment.empty()) {
81 for (char** p = environ; p && *p; ++p) {
82 const std::string item(*p);
83 const auto pos = item.find('=');
84 if (pos != std::string::npos && environment.find(item.substr(0, pos)) == environment.end())
85 env_storage.push_back(item);
86 }
87 for (const auto& [key, value] : environment) {
88 if (key.empty() || key.find('=') != std::string::npos || key.find('\0') != std::string::npos || value.find('\0') != std::string::npos) {
89 posix_spawn_file_actions_destroy(&actions);
90 ::close(fd[0]); ::close(fd[1]); return result;
91 }
92 env_storage.push_back(key + "=" + value);
93 }
94 for (auto& item : env_storage) envp.push_back(item.data());
95 envp.push_back(nullptr);
96 }
97
98 pid_t child = -1;
99 const int err = posix_spawnp(&child, args[0].c_str(), &actions, nullptr,
100 argv.data(), environment.empty() ? environ : envp.data());
101 posix_spawn_file_actions_destroy(&actions);
102 ::close(fd[1]);
103 if (err != 0) { ::close(fd[0]); return result; }
104 result.started = true;
105 char buf[4096];
106 std::string pending;
107 std::size_t total = 0;
108 bool stopped = false;
109 for (;;) {
110 const ssize_t n = ::read(fd[0], buf, sizeof(buf));
111 if (n < 0 && errno == EINTR) continue;
112 if (n <= 0) break;
113 total += static_cast<std::size_t>(n);
114 if (total > limit) { result.truncated = true; stopped = true; break; }
115 if (on_line) {
116 pending.append(buf, static_cast<std::size_t>(n));
117 std::size_t p;
118 while ((p = pending.find('\n')) != std::string::npos) {
119 std::string line = pending.substr(0, p + 1);
120 pending.erase(0, p + 1);
121 if (!on_line(line)) { stopped = true; break; }
122 }
123 if (stopped) break;
124 // Guard against never-ending lines.
125 if (pending.size() > 65536) { result.truncated = true; stopped = true; break; }
126 } else {
127 result.output.append(buf, static_cast<std::size_t>(n));
128 }
129 }
130 if (on_line && !stopped && !pending.empty()) on_line(pending);
131 if (stopped) ::kill(child, SIGTERM);
132 ::close(fd[0]);
133 int status = 0;
134 pid_t waited;
135 do { waited = ::waitpid(child, &status, 0); } while (waited == -1 && errno == EINTR);
136 if (waited == child) {
137 if (WIFEXITED(status)) result.exit_code = WEXITSTATUS(status);
138 else if (WIFSIGNALED(status)) result.exit_code = 128 + WTERMSIG(status);
139 }
140 if (stopped) result.truncated = true;
141 return result;
142}
143} // namespace bg_process
char ** environ
Result run(const std::vector< std::string > &args, bool merge_stderr=false, std::size_t limit=8U *1024U *1024U, const std::map< std::string, std::string > &environment={}, const std::function< bool(const std::string &)> &on_line={})
bool exists_on_path(const std::string &name)