1 // Copyright 2024 The Kyua Authors.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 // * Neither the name of Google Inc. nor the names of its contributors
14 // may be used to endorse or promote products derived from this software
15 // without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 #include "os/freebsd/utils/jail.hpp"
30
31 extern "C" {
32 #include <limits.h>
33 #include <unistd.h>
34 #include <sys/stat.h>
35
36 // FreeBSD sysctl facility
37 #include <sys/sysctl.h>
38
39 // FreeBSD Jail syscalls
40 #include <sys/param.h>
41 #include <sys/jail.h>
42
43 // FreeBSD Jail library
44 #include <jail.h>
45 }
46
47 #include <fstream>
48 #include <iostream>
49 #include <regex>
50
51 #include "model/metadata.hpp"
52 #include "model/test_case.hpp"
53 #include "model/test_program.hpp"
54 #include "utils/fs/path.hpp"
55 #include "utils/process/child.ipp"
56 #include "utils/format/macros.hpp"
57 #include "utils/process/operations.hpp"
58 #include "utils/process/status.hpp"
59
60 namespace process = utils::process;
61 namespace fs = utils::fs;
62
63 using utils::process::args_vector;
64 using utils::process::child;
65
66
67 static const size_t jail_name_max_len = MAXHOSTNAMELEN - 1;
68 static const char* jail_name_prefix = "kyua";
69
70
71 /// Functor to run a program.
72 class run {
73 /// Program binary absolute path.
74 const utils::fs::path& _program;
75
76 /// Program arguments.
77 const args_vector& _args;
78
79 public:
80 /// Constructor.
81 ///
82 /// \param program Program binary absolute path.
83 /// \param args Program arguments.
run(const utils::fs::path & program,const args_vector & args)84 run(
85 const utils::fs::path& program,
86 const args_vector& args) :
87 _program(program),
88 _args(args)
89 {
90 }
91
92 /// Body of the subprocess.
93 void
operator ()(void)94 operator()(void)
95 {
96 process::exec(_program, _args);
97 }
98 };
99
100
101 namespace freebsd {
102 namespace utils {
103
104
105 std::vector< std::string >
parse_params_string(const std::string & str)106 jail::parse_params_string(const std::string& str)
107 {
108 std::vector< std::string > params;
109 std::string p;
110 char quote = 0;
111
112 std::istringstream iss(str);
113 while (iss >> p) {
114 if (p.front() == '"' || p.front() == '\'') {
115 quote = p.front();
116 p.erase(p.begin());
117 if (p.find(quote) == std::string::npos) {
118 std::string rest;
119 std::getline(iss, rest, quote);
120 p += rest;
121 iss.ignore();
122 }
123 if (p.back() == quote)
124 p.erase(p.end() - 1);
125 }
126 params.push_back(p);
127 }
128
129 return params;
130 }
131
132
133 /// Constructs a jail name based on program and test case.
134 ///
135 /// The formula is "kyua" + <program path> + "_" + <test case name>.
136 /// All non-alphanumeric chars are replaced with "_".
137 ///
138 /// If a resulting string exceeds maximum allowed length of a jail name,
139 /// then it's shortened from the left side keeping the "kyua" prefix.
140 ///
141 /// \param program The test program.
142 /// \param test_case_name Name of the test case.
143 ///
144 /// \return A jail name string.
145 std::string
make_name(const fs::path & program,const std::string & test_case_name)146 jail::make_name(const fs::path& program,
147 const std::string& test_case_name)
148 {
149 std::string name = std::regex_replace(
150 program.str() + "_" + test_case_name,
151 std::regex(R"([^A-Za-z0-9_])"),
152 "_");
153
154 const std::string::size_type limit =
155 jail_name_max_len - strlen(jail_name_prefix);
156 if (name.length() > limit)
157 name.erase(0, name.length() - limit);
158
159 return jail_name_prefix + name;
160 }
161
162
163 /// Create a jail with a given name and params string.
164 ///
165 /// A new jail will always be 'persist', thus the caller is expected to remove
166 /// the jail eventually via remove().
167 ///
168 /// It's expected to be run in a subprocess.
169 ///
170 /// \param jail_name Name of a new jail.
171 /// \param jail_params String of jail parameters.
172 void
create(const std::string & jail_name,const std::string & jail_params)173 jail::create(const std::string& jail_name,
174 const std::string& jail_params)
175 {
176 args_vector av;
177
178 // creation flag
179 av.push_back("-qc");
180
181 // jail name
182 av.push_back("name=" + jail_name);
183
184 // determine maximum allowed children.max
185 const char* const oid = "security.jail.children.max";
186 int max;
187 size_t len = sizeof(max);
188 if (::sysctlbyname(oid, &max, &len, NULL, 0) != 0) {
189 std::cerr << "sysctlbyname(" << oid << ") errors: "
190 << strerror(errno) << ".\n";
191 std::exit(EXIT_FAILURE);
192 }
193 if (len < sizeof(max)) {
194 std::cerr << "sysctlbyname(" << oid << ") provides less "
195 "data (" << len << ") than expected (" << sizeof(max) << ").\n";
196 std::exit(EXIT_FAILURE);
197 }
198 if (max < 0) {
199 std::cerr << "sysctlbyname(" << oid << ") yields "
200 "abnormal " << max << ".\n";
201 std::exit(EXIT_FAILURE);
202 }
203 if (max > 0)
204 max--; // a child jail must have less than parent's children.max
205 av.push_back("children.max=" + std::to_string(max));
206
207 // test defined jail params
208 const std::vector< std::string > params = parse_params_string(jail_params);
209 for (const std::string& p : params)
210 av.push_back(p);
211
212 // it must be persist
213 av.push_back("persist");
214
215 // invoke jail
216 std::auto_ptr< process::child > child = child::fork_capture(
217 run(fs::path("/usr/sbin/jail"), av));
218 process::status status = child->wait();
219
220 // expect success
221 if (status.exited() && status.exitstatus() == EXIT_SUCCESS)
222 return;
223
224 // otherwise, let us know what jail thinks and fail fast
225 std::cerr << child->output().rdbuf();
226 std::exit(EXIT_FAILURE);
227 }
228
229
230 /// Executes an external binary in a jail and replaces the current process.
231 ///
232 /// \param jail_name Name of the jail to run within.
233 /// \param program The test program binary absolute path.
234 /// \param args The arguments to pass to the binary, without the program name.
235 void
exec(const std::string & jail_name,const fs::path & program,const args_vector & args)236 jail::exec(const std::string& jail_name,
237 const fs::path& program,
238 const args_vector& args) throw()
239 {
240 // get work dir prepared by kyua
241 char cwd[PATH_MAX];
242 if (::getcwd(cwd, sizeof(cwd)) == NULL) {
243 std::cerr << "jail::exec: getcwd() errors: "
244 << strerror(errno) << ".\n";
245 std::exit(EXIT_FAILURE);
246 }
247
248 // get jail id by its name
249 int jid = ::jail_getid(jail_name.c_str());
250 if (jid == -1) {
251 std::cerr << "jail::exec: jail_getid() errors: "
252 << strerror(errno) << ": " << jail_errmsg << ".\n";
253 std::exit(EXIT_FAILURE);
254 }
255
256 // attach to the jail
257 if (::jail_attach(jid) == -1) {
258 std::cerr << "jail::exec: jail_attach() errors: "
259 << strerror(errno) << ".\n";
260 std::exit(EXIT_FAILURE);
261 }
262
263 // set back the expected work dir
264 if (::chdir(cwd) == -1) {
265 std::cerr << "jail::exec: chdir() errors: "
266 << strerror(errno) << ".\n";
267 std::exit(EXIT_FAILURE);
268 }
269
270 process::exec(program, args);
271 }
272
273
274 /// Removes a jail with a given name.
275 ///
276 /// It's expected to be run in a subprocess.
277 ///
278 /// \param jail_name Name of a jail to remove.
279 void
remove(const std::string & jail_name)280 jail::remove(const std::string& jail_name)
281 {
282 args_vector av;
283
284 // removal flag
285 av.push_back("-r");
286
287 // jail name
288 av.push_back(jail_name);
289
290 // invoke jail
291 std::auto_ptr< process::child > child = child::fork_capture(
292 run(fs::path("/usr/sbin/jail"), av));
293 process::status status = child->wait();
294
295 // expect success
296 if (status.exited() && status.exitstatus() == EXIT_SUCCESS)
297 std::exit(EXIT_SUCCESS);
298
299 // otherwise, let us know what jail thinks and fail fast
300 std::cerr << child->output().rdbuf();
301 std::exit(EXIT_FAILURE);
302 }
303
304
305 } // namespace utils
306 } // namespace freebsd
307