xref: /freebsd/contrib/kyua/os/freebsd/utils/jail.cpp (revision 257e70f1d5ee61037c8c59b116538d3b6b1427a2)
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