Skip to content
API Documentation
BastionGuard™
SystemServiceControl.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-3.0-or-later
3
4#include <array>
5#include <cerrno>
6#include <cstring>
7#include <filesystem>
8#include <sstream>
9#include <sys/types.h>
10#include <sys/wait.h>
11#include <unistd.h>
12
14namespace {
15
17 int exitCode{127};
18 std::string output;
19};
20
21bool executableExists(const char* path) noexcept
22{
23 return path && ::access(path, X_OK) == 0;
24}
25
26std::string firstExecutable(std::initializer_list<const char*> candidates)
27{
28 for (const char* candidate : candidates) {
29 if (executableExists(candidate))
30 return candidate;
31 }
32 return {};
33}
34
35CommandResult runCommand(const std::vector<std::string>& arguments) noexcept
36{
37 CommandResult result;
38 if (arguments.empty())
39 return result;
40
41 int descriptors[2]{};
42 if (::pipe(descriptors) != 0)
43 return result;
44
45 const pid_t child = ::fork();
46 if (child < 0) {
47 ::close(descriptors[0]);
48 ::close(descriptors[1]);
49 return result;
50 }
51
52 if (child == 0) {
53 ::close(descriptors[0]);
54 ::dup2(descriptors[1], STDOUT_FILENO);
55 ::dup2(descriptors[1], STDERR_FILENO);
56 ::close(descriptors[1]);
57
58 std::vector<char*> argv;
59 argv.reserve(arguments.size() + 1);
60 for (const auto& argument : arguments)
61 argv.push_back(const_cast<char*>(argument.c_str()));
62 argv.push_back(nullptr);
63 ::execv(argv.front(), argv.data());
64 _exit(127);
65 }
66
67 ::close(descriptors[1]);
68 std::array<char, 2048> buffer{};
69 for (;;) {
70 const ssize_t count = ::read(descriptors[0], buffer.data(), buffer.size());
71 if (count > 0) {
72 result.output.append(buffer.data(), static_cast<std::size_t>(count));
73 continue;
74 }
75 if (count < 0 && errno == EINTR)
76 continue;
77 break;
78 }
79 ::close(descriptors[0]);
80
81 int status = 0;
82 while (::waitpid(child, &status, 0) < 0 && errno == EINTR) {
83 }
84 if (WIFEXITED(status))
85 result.exitCode = WEXITSTATUS(status);
86 else if (WIFSIGNALED(status))
87 result.exitCode = 128 + WTERMSIG(status);
88 return result;
89}
90
91bool startsWithPath(const std::filesystem::path& path,
92 const std::filesystem::path& prefix) noexcept
93{
94 const auto normalizedPath = path.lexically_normal();
95 const auto normalizedPrefix = prefix.lexically_normal();
96 auto pathIt = normalizedPath.begin();
97 for (auto prefixIt = normalizedPrefix.begin();
98 prefixIt != normalizedPrefix.end(); ++prefixIt, ++pathIt) {
99 if (pathIt == normalizedPath.end() || *pathIt != *prefixIt)
100 return false;
101 }
102 return true;
103}
104
105bool isSystemdUnitExtension(const std::string& extension)
106{
107 return extension == ".service" || extension == ".socket" ||
108 extension == ".timer" || extension == ".path" ||
109 extension == ".mount" || extension == ".automount" ||
110 extension == ".target";
111}
112
113std::optional<std::string> systemdUnitName(const std::filesystem::path& path)
114{
115 if (isSystemdUnitExtension(path.extension().string()))
116 return path.filename().string();
117
118 for (auto current = path.parent_path(); !current.empty() && current != current.root_path();
119 current = current.parent_path()) {
120 const std::string name = current.filename().string();
121 if (name.size() > 2 && name.compare(name.size() - 2, 2, ".d") == 0) {
122 const std::string unit = name.substr(0, name.size() - 2);
123 if (isSystemdUnitExtension(std::filesystem::path(unit).extension().string()))
124 return unit;
125 }
126 }
127 return std::nullopt;
128}
129
130ServiceActionResult resultFrom(const std::string& operation,
131 const ServiceTarget& target,
132 const std::vector<CommandResult>& results)
133{
134 bool success = true;
135 std::ostringstream message;
136 message << operation << " " << target.name << " using " << toString(target.init);
137 for (const auto& result : results) {
138 if (result.exitCode != 0)
139 success = false;
140 if (!result.output.empty())
141 message << ": " << result.output;
142 }
143 return {success, message.str()};
144}
145
146} // namespace
147
149{
150 try {
151 if (std::filesystem::exists("/run/systemd/system") &&
152 !firstExecutable({"/usr/bin/systemctl", "/bin/systemctl"}).empty())
153 return InitSystem::Systemd;
154 if (!firstExecutable({"/sbin/openrc-run", "/usr/bin/openrc-run"}).empty() &&
155 !firstExecutable({"/sbin/rc-service", "/usr/bin/rc-service"}).empty())
156 return InitSystem::OpenRC;
157 if (!firstExecutable({"/usr/bin/dinitctl", "/bin/dinitctl"}).empty())
158 return InitSystem::Dinit;
159 if (std::filesystem::exists("/etc/init.d"))
161 } catch (...) {
162 }
163 return InitSystem::None;
164}
165
166const char* toString(const InitSystem init) noexcept
167{
168 switch (init) {
169 case InitSystem::Systemd: return "systemd";
170 case InitSystem::OpenRC: return "openrc";
171 case InitSystem::Dinit: return "dinit";
172 case InitSystem::SysVInit: return "sysvinit";
173 case InitSystem::None: return "none";
174 }
175 return "none";
176}
177
178std::optional<ServiceTarget>
179identifySystemService(const std::filesystem::path& input) noexcept
180{
181 try {
182 if (input.empty() || !input.is_absolute())
183 return std::nullopt;
184 const auto path = input.lexically_normal();
185 const auto init = detectInitSystem();
186
187 const bool systemdPath =
188 startsWithPath(path, "/etc/systemd/system") ||
189 startsWithPath(path, "/usr/lib/systemd/system") ||
190 startsWithPath(path, "/lib/systemd/system");
191 if (systemdPath && init == InitSystem::Systemd) {
192 if (const auto unit = systemdUnitName(path))
193 return ServiceTarget{InitSystem::Systemd, *unit, path};
194 }
195
196 if (startsWithPath(path, "/etc/conf.d") &&
197 init == InitSystem::OpenRC && path.has_filename())
198 return ServiceTarget{InitSystem::OpenRC, path.filename().string(), path};
199
200 if (startsWithPath(path, "/etc/init.d") && path.has_filename()) {
201 const auto detected = init == InitSystem::OpenRC
203 return ServiceTarget{detected, path.filename().string(), path};
204 }
205
206 if ((startsWithPath(path, "/etc/dinit.d") ||
207 startsWithPath(path, "/usr/lib/dinit.d")) &&
208 init == InitSystem::Dinit && path.has_filename())
209 return ServiceTarget{InitSystem::Dinit, path.filename().string(), path};
210 } catch (...) {
211 }
212 return std::nullopt;
213}
214
215ServiceActionResult blockSystemService(const std::filesystem::path& path) noexcept
216{
217 const auto target = identifySystemService(path);
218 if (!target)
219 return {false, "The path is not a recognized system service definition."};
220 if (target->name == "bastionguard-rootguard.service" ||
221 target->name == "bastionguard-rootguard")
222 return {false, "RootGuard refuses to block its own service."};
223
224 std::vector<CommandResult> results;
225 switch (target->init) {
226 case InitSystem::Systemd: {
227 const auto systemctl = firstExecutable({"/usr/bin/systemctl", "/bin/systemctl"});
228 if (systemctl.empty())
229 return {false, "systemctl is unavailable."};
230 results.push_back(runCommand({systemctl, "stop", target->name}));
231 results.push_back(runCommand({systemctl, "mask", "--runtime", target->name}));
232 break;
233 }
234 case InitSystem::OpenRC: {
235 const auto rcService = firstExecutable({"/sbin/rc-service", "/usr/bin/rc-service"});
236 if (rcService.empty())
237 return {false, "rc-service is unavailable."};
238 results.push_back(runCommand({rcService, target->name, "stop"}));
239 break;
240 }
241 case InitSystem::Dinit: {
242 const auto dinitctl = firstExecutable({"/usr/bin/dinitctl", "/bin/dinitctl"});
243 if (dinitctl.empty())
244 return {false, "dinitctl is unavailable."};
245 results.push_back(runCommand({dinitctl, "stop", target->name}));
246 break;
247 }
249 results.push_back(runCommand({target->definition.string(), "stop"}));
250 break;
251 case InitSystem::None:
252 return {false, "No supported init system was detected."};
253 }
254 return resultFrom("Blocked", *target, results);
255}
256
257ServiceActionResult unblockSystemService(const std::filesystem::path& path) noexcept
258{
259 const auto target = identifySystemService(path);
260 if (!target)
261 return {true, "The path is not a system service definition."};
262
263 std::vector<CommandResult> results;
264 switch (target->init) {
265 case InitSystem::Systemd: {
266 const auto systemctl = firstExecutable({"/usr/bin/systemctl", "/bin/systemctl"});
267 if (systemctl.empty())
268 return {false, "systemctl is unavailable."};
269 results.push_back(runCommand({systemctl, "unmask", "--runtime", target->name}));
270 break;
271 }
275 return {true, std::string("The service remains stopped; no runtime mask is used by ") +
276 toString(target->init) + "."};
277 case InitSystem::None:
278 return {false, "No supported init system was detected."};
279 }
280 return resultFrom("Unblocked", *target, results);
281}
282
283} // namespace bastionguard::rootguard
CommandResult runCommand(const std::vector< std::string > &arguments) noexcept
bool startsWithPath(const std::filesystem::path &path, const std::filesystem::path &prefix) noexcept
std::string firstExecutable(std::initializer_list< const char * > candidates)
ServiceActionResult resultFrom(const std::string &operation, const ServiceTarget &target, const std::vector< CommandResult > &results)
std::optional< std::string > systemdUnitName(const std::filesystem::path &path)
std::optional< ServiceTarget > identifySystemService(const std::filesystem::path &path) noexcept
InitSystem detectInitSystem() noexcept
const char * toString(Mode mode) noexcept
Definition Policy.cpp:6
ServiceActionResult blockSystemService(const std::filesystem::path &path) noexcept
ServiceActionResult unblockSystemService(const std::filesystem::path &path) noexcept