diff --git a/contrib/kyua/engine/atf.cpp b/contrib/kyua/engine/atf.cpp --- a/contrib/kyua/engine/atf.cpp +++ b/contrib/kyua/engine/atf.cpp @@ -39,6 +39,7 @@ #include "engine/atf_list.hpp" #include "engine/atf_result.hpp" #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" @@ -190,7 +191,9 @@ args.push_back(F("-r%s") % (control_directory / result_name)); args.push_back(test_case_name); - process::exec(test_program.absolute_path(), args); + + engine::execenv::init(test_program, test_case_name); + engine::execenv::exec(test_program, test_case_name, args); } @@ -219,7 +222,8 @@ } args.push_back(F("%s:cleanup") % test_case_name); - process::exec(test_program.absolute_path(), args); + + engine::execenv::exec(test_program, test_case_name, args); } diff --git a/contrib/kyua/engine/atf_list.cpp b/contrib/kyua/engine/atf_list.cpp --- a/contrib/kyua/engine/atf_list.cpp +++ b/contrib/kyua/engine/atf_list.cpp @@ -121,6 +121,10 @@ mdbuilder.set_string("has_cleanup", value); } else if (name == "require.arch") { mdbuilder.set_string("allowed_architectures", value); + } else if (name == "execenv") { + mdbuilder.set_string("execenv", value); + } else if (name == "execenv.jail") { + mdbuilder.set_string("execenv_jail", value); } else if (name == "require.config") { mdbuilder.set_string("required_configs", value); } else if (name == "require.files") { diff --git a/contrib/kyua/engine/execenv/execenv.hpp b/contrib/kyua/engine/execenv/execenv.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv.hpp @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/execenv/execenv.hpp +/// Execution environment multiplexer. +/// +/// A test case may ask for a specific execution environment like running in +/// a jail, what needs initialization before the test run and cleanup after. +/// +/// By default, there is no specific execution environment, so called host +/// environment, and no additional initialization or cleanup is done. + +#if !defined(ENGINE_EXECENV_EXECENV_HPP) +#define ENGINE_EXECENV_EXECENV_HPP + +#include "model/test_program.hpp" +#include "utils/defs.hpp" +#include "utils/process/operations_fwd.hpp" + +namespace engine { +namespace execenv { + + +void init(const model::test_program&, const std::string&); + +void exec(const model::test_program&, const std::string&, + const utils::process::args_vector&) throw() UTILS_NORETURN; + +void cleanup(const model::test_program&, const std::string&); + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HPP) diff --git a/contrib/kyua/engine/execenv/execenv.cpp b/contrib/kyua/engine/execenv/execenv.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv.cpp @@ -0,0 +1,103 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/execenv/execenv.hpp" + +#include "engine/execenv/jail.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/operations.hpp" + +namespace execenv = engine::execenv; +namespace process = utils::process; + +using utils::process::args_vector; + + +/// Initialize execution environment. +/// +/// It's expected to be called inside a fork which runs interface::exec_test(), +/// so we can fail a test fast if its execution environment setup fails, and +/// test execution could use the configured proc environment, if expected. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +execenv::init(const model::test_program& test_program, + const std::string& test_case_name) +{ + const model::test_case& test_case = test_program.find(test_case_name); + if (test_case.get_metadata().is_execenv_jail()) + return execenv::jail::init(test_program, test_case_name); + // else if ...other env + + // host environment by default + return; +} + + +/// Execute within an execution environment. +/// +/// It's expected to be called inside a fork which runs interface::exec_test(). +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +execenv::exec(const model::test_program& test_program, + const std::string& test_case_name, + const args_vector& args) throw() +{ + const model::test_case& test_case = test_program.find(test_case_name); + if (test_case.get_metadata().is_execenv_jail()) + execenv::jail::exec(test_program, test_case_name, args); + // else if ...other env + + // host environment by default + process::exec(test_program.absolute_path(), args); +} + + +/// Cleanup execution environment. +/// +/// It's expected to be called inside a fork for execenv cleanup. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +execenv::cleanup(const model::test_program& test_program, + const std::string& test_case_name) +{ + const model::test_case& test_case = test_program.find(test_case_name); + if (test_case.get_metadata().is_execenv_jail()) + return execenv::jail::cleanup(test_program, test_case_name); + // else if ...other env + + // cleanup is not expected to be called for host environment + std::exit(EXIT_SUCCESS); +} diff --git a/contrib/kyua/engine/execenv/jail.hpp b/contrib/kyua/engine/execenv/jail.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/jail.hpp @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/execenv/jail.hpp +/// FreeBSD jail execution environment. + +#if !defined(ENGINE_EXECENV_JAIL_HPP) +#define ENGINE_EXECENV_JAIL_HPP + +#include "model/test_program.hpp" +#include "utils/defs.hpp" +#include "utils/process/operations_fwd.hpp" + +namespace engine { +namespace execenv { +namespace jail { + + +void init(const model::test_program&, const std::string&); + +void exec(const model::test_program&, const std::string&, + const utils::process::args_vector&) throw() UTILS_NORETURN; + +void cleanup(const model::test_program&, const std::string&); + + +} // namespace jail +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_JAIL_HPP) diff --git a/contrib/kyua/engine/execenv/jail.cpp b/contrib/kyua/engine/execenv/jail.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/jail.cpp @@ -0,0 +1,88 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/execenv/jail.hpp" + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/jail.hpp" +#include "utils/process/operations.hpp" + +namespace execenv = engine::execenv; +namespace process = utils::process; + +using utils::process::args_vector; + + +/// Initialize execution environment. +/// +/// It's expected to be called inside a fork which runs interface::exec_test(), +/// so we can fail a test fast if its execution environment setup fails. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +execenv::jail::init(const model::test_program& test_program, + const std::string& test_case_name) +{ + const model::test_case& test_case = test_program.find(test_case_name); + + process::jail::create(test_program.absolute_path(), test_case_name, + test_case.get_metadata().execenv_jail()); +} + + +/// Execute within an execution environment. +/// +/// It's expected to be called inside a fork which runs interface::exec_test(). +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +execenv::jail::exec(const model::test_program& test_program, + const std::string& test_case_name, + const args_vector& args) throw() +{ + process::jail::exec(test_program.absolute_path(), test_case_name, + args); +} + + +/// Cleanup execution environment. +/// +/// It's expected to be called inside a fork for execenv cleanup. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +execenv::jail::cleanup(const model::test_program& test_program, + const std::string& test_case_name) +{ + process::jail::remove(test_program.absolute_path(), test_case_name); +} diff --git a/contrib/kyua/engine/plain.cpp b/contrib/kyua/engine/plain.cpp --- a/contrib/kyua/engine/plain.cpp +++ b/contrib/kyua/engine/plain.cpp @@ -34,6 +34,7 @@ #include +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" @@ -104,7 +105,9 @@ } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + engine::execenv::init(test_program, test_case_name); + engine::execenv::exec(test_program, test_case_name, args); } diff --git a/contrib/kyua/engine/scheduler.hpp b/contrib/kyua/engine/scheduler.hpp --- a/contrib/kyua/engine/scheduler.hpp +++ b/contrib/kyua/engine/scheduler.hpp @@ -262,6 +262,7 @@ extern utils::datetime::delta cleanup_timeout; +extern utils::datetime::delta execenv_cleanup_timeout; extern utils::datetime::delta list_timeout; diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp --- a/contrib/kyua/engine/scheduler.cpp +++ b/contrib/kyua/engine/scheduler.cpp @@ -40,6 +40,7 @@ #include "engine/config.hpp" #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "engine/requirements.hpp" #include "model/context.hpp" #include "model/metadata.hpp" @@ -87,6 +88,10 @@ datetime::delta scheduler::cleanup_timeout(60, 0); +/// Timeout for the test case execenv cleanup operation. +datetime::delta scheduler::execenv_cleanup_timeout(60, 0); + + /// Timeout for the test case listing operation. /// /// TODO(jmmv): This is here only for testing purposes. Maybe we should expose @@ -206,6 +211,18 @@ /// denote that no further attempts shall be made at cleaning this up. bool needs_cleanup; + /// Whether this test case still needs to have its execenv cleanup executed. + /// + /// This is set externally when the cleanup routine is actually invoked to + /// denote that no further attempts shall be made at cleaning this up. + bool needs_execenv_cleanup; + + /// Original PID of the test case subprocess. + /// + /// This is used for the cleanup upon termination by a signal, to reap the + /// leftovers and form missing exit_handle. + int pid; + /// The exit_handle for this test once it has completed. /// /// This is set externally when the test case has finished, as we need this @@ -222,12 +239,14 @@ test_exec_data(const model::test_program_ptr test_program_, const std::string& test_case_name_, const std::shared_ptr< scheduler::interface > interface_, - const config::tree& user_config_) : + const config::tree& user_config_, + const int pid_) : exec_data(test_program_, test_case_name_), - interface(interface_), user_config(user_config_) + interface(interface_), user_config(user_config_), pid(pid_) { const model::test_case& test_case = test_program->find(test_case_name); needs_cleanup = test_case.get_metadata().has_cleanup(); + needs_execenv_cleanup = test_case.get_metadata().has_execenv(); } }; @@ -266,6 +285,40 @@ }; +/// Maintenance data held while a test execenv cleanup is being executed. +/// +/// Instances of this object are related to a previous test_exec_data, as +/// cleanup routines can only exist once the test has been run. +struct execenv_exec_data : public exec_data { + /// The exit handle of the test. This is necessary so that we can return + /// the correct exit_handle to the user of the scheduler. + executor::exit_handle body_exit_handle; + + /// The final result of the test's body. This is necessary to compute the + /// right return value for a test with a cleanup routine: the body result is + /// respected if it is a "bad" result; else the result of the cleanup + /// routine is used if it has failed. + model::test_result body_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param body_exit_handle_ If not none, exit handle of the body + /// corresponding to the cleanup routine represented by this exec_data. + /// \param body_result_ If not none, result of the body corresponding to the + /// cleanup routine represented by this exec_data. + execenv_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const executor::exit_handle& body_exit_handle_, + const model::test_result& body_result_) : + exec_data(test_program_, test_case_name_), + body_exit_handle(body_exit_handle_), body_result(body_result_) + { + } +}; + + /// Shared pointer to exec_data. /// /// We require this because we want exec_data to not be copyable, and thus we @@ -492,6 +545,39 @@ }; +/// Functor to execute a test execenv cleanup in a child process. +class run_execenv_cleanup { + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + +public: + /// Constructor. + /// + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + run_execenv_cleanup( + const model::test_program_ptr test_program, + const std::string& test_case_name) : + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where cleanup will be + /// run from. + void + operator()(const fs::path& /* control_directory */) + { + engine::execenv::cleanup(_test_program, _test_case_name); + } +}; + + /// Obtains the right scheduler interface for a given test program. /// /// \param name The name of the interface of the test program. @@ -835,6 +921,22 @@ % test_data->test_case_name); } } + + const test_exec_data_vector td = tests_needing_execenv_cleanup(); + + for (test_exec_data_vector::const_iterator iter = td.begin(); + iter != td.end(); ++iter) { + const test_exec_data* test_data = *iter; + + try { + sync_execenv_cleanup(test_data); + } catch (const std::runtime_error& e) { + LW(F("Failed to run execenv cleanup routine for %s:%s on abrupt " + "termination") + % test_data->test_program->relative_path() + % test_data->test_case_name); + } + } } /// Finds any pending exec_datas that correspond to tests needing cleanup. @@ -856,6 +958,8 @@ if (test_data->needs_cleanup) { tests_data.push_back(test_data); test_data->needs_cleanup = false; + if (!test_data->exit_handle) + test_data->exit_handle = generic.reap(test_data->pid); } } catch (const std::bad_cast& e) { // Do nothing for cleanup_exec_data objects. @@ -865,6 +969,37 @@ return tests_data; } + /// Finds any pending exec_datas that correspond to tests needing execenv + /// cleanup. + /// + /// \return The collection of test_exec_data objects that have their + /// specific execenv property set. + test_exec_data_vector + tests_needing_execenv_cleanup(void) + { + test_exec_data_vector tests_data; + + for (exec_data_map::const_iterator iter = all_exec_data.begin(); + iter != all_exec_data.end(); ++iter) { + const exec_data_ptr data = (*iter).second; + + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + if (test_data->needs_execenv_cleanup) { + tests_data.push_back(test_data); + test_data->needs_execenv_cleanup = false; + if (!test_data->exit_handle) + test_data->exit_handle = generic.reap(test_data->pid); + } + } catch (const std::bad_cast& e) { + // Do nothing for other objects. + } + } + + return tests_data; + } + /// Cleans up a single test case synchronously. /// /// \param test_data The data of the previously executed test case to be @@ -926,6 +1061,61 @@ return handle; } + + /// Cleans up a single test case execenv synchronously. + /// + /// \param test_data The data of the previously executed test case to be + /// cleaned up. + void + sync_execenv_cleanup(const test_exec_data* test_data) + { + // The message in this result should never be seen by the user, but use + // something reasonable just in case it leaks and we need to pinpoint + // the call site. + model::test_result result(model::test_result_broken, + "Test case died abruptly"); + + const executor::exec_handle cleanup_handle = spawn_execenv_cleanup( + test_data->test_program, test_data->test_case_name, + test_data->exit_handle.get(), result); + generic.wait(cleanup_handle); + } + + /// Forks and executes a test case execenv cleanup asynchronously. + /// + /// \param test_program The container test program. + /// \param test_case_name The name of the test case to run. + /// \param body_handle The exit handle of the test case's corresponding + /// body. The cleanup will be executed in the same context. + /// \param body_result The result of the test case's corresponding body. + /// + /// \return A handle for the background operation. Used to match the result + /// of the execution returned by wait_any() with this invocation. + executor::exec_handle + spawn_execenv_cleanup(const model::test_program_ptr test_program, + const std::string& test_case_name, + const executor::exit_handle& body_handle, + const model::test_result& body_result) + { + generic.check_interrupt(); + + LI(F("Spawning %s:%s (execenv cleanup)") + % test_program->absolute_path() % test_case_name); + + const executor::exec_handle handle = generic.spawn_followup( + run_execenv_cleanup(test_program, test_case_name), + body_handle, execenv_cleanup_timeout); + + const exec_data_ptr data(new execenv_exec_data( + test_program, test_case_name, body_handle, body_result)); + LD(F("Inserting %s into all_exec_data (execenv cleanup)") % handle.pid()); + INV_MSG(all_exec_data.find(handle.pid()) == all_exec_data.end(), + F("PID %s already in all_exec_data; not properly cleaned " + "up or reused too fast") % handle.pid());; + all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle; + } }; @@ -1115,7 +1305,7 @@ unprivileged_user); const exec_data_ptr data(new test_exec_data( - test_program, test_case_name, interface, user_config)); + test_program, test_case_name, interface, user_config, handle.pid())); LD(F("Inserting %s into all_exec_data") % handle.pid()); INV_MSG( _pimpl->all_exec_data.find(handle.pid()) == _pimpl->all_exec_data.end(), @@ -1150,6 +1340,8 @@ _pimpl->generic, handle); optional< model::test_result > result; + + // test itself try { test_exec_data* test_data = &dynamic_cast< test_exec_data& >( *data.get()); @@ -1185,6 +1377,7 @@ // if the test's body reports a skip (because actions could have // already been taken). test_data->needs_cleanup = false; + test_data->needs_execenv_cleanup = false; } } if (!result) { @@ -1209,7 +1402,6 @@ _pimpl->spawn_cleanup(test_data->test_program, test_data->test_case_name, test_data->user_config, handle, result.get()); - test_data->needs_cleanup = false; // TODO(jmmv): Chaining this call is ugly. We'd be better off by // looping over terminated processes until we got a result suitable @@ -1218,7 +1410,21 @@ // of test cases do not have cleanup routines. return wait_any(); } + + if (test_data->needs_execenv_cleanup) { + INV(test_case.get_metadata().has_execenv()); + _pimpl->spawn_execenv_cleanup(test_data->test_program, + test_data->test_case_name, + handle, result.get()); + test_data->needs_execenv_cleanup = false; + return wait_any(); + } } catch (const std::bad_cast& e) { + // ignored + } + + // test cleanup + try { const cleanup_exec_data* cleanup_data = &dynamic_cast< const cleanup_exec_data& >(*data.get()); LD(F("Got %s from all_exec_data (cleanup)") % handle.original_pid()); @@ -1257,7 +1463,65 @@ _pimpl->all_exec_data.erase(handle.original_pid()); handle = cleanup_data->body_exit_handle; + + const exec_data_map::iterator it = _pimpl->all_exec_data.find( + handle.original_pid()); + if (it != _pimpl->all_exec_data.end()) { + exec_data_ptr d = (*it).second; + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *d.get()); + const model::test_case& test_case = + cleanup_data->test_program->find(cleanup_data->test_case_name); + test_data->needs_cleanup = false; + + if (test_data->needs_execenv_cleanup) { + INV(test_case.get_metadata().has_execenv()); + _pimpl->spawn_execenv_cleanup(cleanup_data->test_program, + cleanup_data->test_case_name, + handle, result.get()); + test_data->needs_execenv_cleanup = false; + return wait_any(); + } + } + } catch (const std::bad_cast& e) { + // ignored } + + // execenv cleanup + try { + const execenv_exec_data* execenv_data = + &dynamic_cast< const execenv_exec_data& >(*data.get()); + LD(F("Got %s from all_exec_data (execenv cleanup)") % handle.original_pid()); + + const model::test_result& body_result = execenv_data->body_result; + if (body_result.good()) { + if (!handle.status()) { + result = model::test_result(model::test_result_broken, + "Test case execenv cleanup timed out"); + } else { + if (!handle.status().get().exited() || + handle.status().get().exitstatus() != EXIT_SUCCESS) { + result = model::test_result( + model::test_result_broken, + "Test case execenv cleanup did not terminate successfully"); // ? + } else { + result = body_result; + } + } + } else { + result = body_result; + } + + LD(F("Removing %s from all_exec_data (execenv cleanup) in favor of %s") + % handle.original_pid() + % execenv_data->body_exit_handle.original_pid()); + _pimpl->all_exec_data.erase(handle.original_pid()); + + handle = execenv_data->body_exit_handle; + } catch (const std::bad_cast& e) { + // ignored + } + INV(result); std::shared_ptr< result_handle::bimpl > result_handle_bimpl( diff --git a/contrib/kyua/engine/tap.cpp b/contrib/kyua/engine/tap.cpp --- a/contrib/kyua/engine/tap.cpp +++ b/contrib/kyua/engine/tap.cpp @@ -35,6 +35,7 @@ #include #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "engine/tap_parser.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" @@ -151,7 +152,9 @@ } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + engine::execenv::init(test_program, test_case_name); + engine::execenv::exec(test_program, test_case_name, args); } diff --git a/contrib/kyua/model/metadata.hpp b/contrib/kyua/model/metadata.hpp --- a/contrib/kyua/model/metadata.hpp +++ b/contrib/kyua/model/metadata.hpp @@ -69,6 +69,10 @@ const std::string& description(void) const; bool has_cleanup(void) const; bool is_exclusive(void) const; + const std::string& execenv(void) const; + bool has_execenv(void) const; + const std::string& execenv_jail(void) const; + bool is_execenv_jail(void) const; const strings_set& required_configs(void) const; const utils::units::bytes& required_disk_space(void) const; const paths_set& required_files(void) const; @@ -112,6 +116,8 @@ metadata_builder& set_description(const std::string&); metadata_builder& set_has_cleanup(const bool); metadata_builder& set_is_exclusive(const bool); + metadata_builder& set_execenv(const std::string&); + metadata_builder& set_execenv_jail(const std::string&); metadata_builder& set_required_configs(const strings_set&); metadata_builder& set_required_disk_space(const utils::units::bytes&); metadata_builder& set_required_files(const paths_set&); diff --git a/contrib/kyua/model/metadata.cpp b/contrib/kyua/model/metadata.cpp --- a/contrib/kyua/model/metadata.cpp +++ b/contrib/kyua/model/metadata.cpp @@ -249,6 +249,8 @@ tree.define< config::string_node >("description"); tree.define< config::bool_node >("has_cleanup"); tree.define< config::bool_node >("is_exclusive"); + tree.define< config::string_node >("execenv"); + tree.define< config::string_node >("execenv_jail"); tree.define< config::strings_set_node >("required_configs"); tree.define< bytes_node >("required_disk_space"); tree.define< paths_set_node >("required_files"); @@ -272,6 +274,8 @@ tree.set< config::string_node >("description", ""); tree.set< config::bool_node >("has_cleanup", false); tree.set< config::bool_node >("is_exclusive", false); + tree.set< config::string_node >("execenv", ""); + tree.set< config::string_node >("execenv_jail", ""); tree.set< config::strings_set_node >("required_configs", model::strings_set()); tree.set< bytes_node >("required_disk_space", units::bytes(0)); @@ -486,13 +490,64 @@ model::metadata::is_exclusive(void) const { if (_pimpl->props.is_set("is_exclusive")) { - return _pimpl->props.lookup< config::bool_node >("is_exclusive"); + const bool is_excl = + _pimpl->props.lookup< config::bool_node >("is_exclusive"); + return is_excl && !is_execenv_jail(); } else { return get_defaults().lookup< config::bool_node >("is_exclusive"); } } +/// Returns execution environment name. +/// +/// \return Name of configured execution environment. +const std::string& +model::metadata::execenv(void) const +{ + if (_pimpl->props.is_set("execenv")) { + return _pimpl->props.lookup< config::string_node >("execenv"); + } else { + return get_defaults().lookup< config::string_node >("execenv"); + } +} + + +/// Returns whether the test has any specific execenv apart from "host" one. +/// +/// \return True if there is a non-host execenv configured; false otherwise. +bool +model::metadata::has_execenv(void) const +{ + const std::string& name = execenv(); + return !name.empty() && name != "host"; +} + + +/// Returns execenv jail parameters string to run a test with. +/// +/// \return String of jail parameters. +const std::string& +model::metadata::execenv_jail(void) const +{ + if (_pimpl->props.is_set("execenv_jail")) { + return _pimpl->props.lookup< config::string_node >("execenv_jail"); + } else { + return get_defaults().lookup< config::string_node >("execenv_jail"); + } +} + + +/// Returns whether the test is configured for jail execenv. +/// +/// \return True if there is a jail execenv is set; false otherwise. +bool +model::metadata::is_execenv_jail(void) const +{ + return execenv() == "jail"; +} + + /// Returns the list of configuration variables needed by the test. /// /// \return Set of configuration variables. @@ -920,6 +975,36 @@ } +/// Sets execution environment name. +/// +/// \param name Execution environment name. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_execenv(const std::string& name) +{ + set< config::string_node >(_pimpl->props, "execenv", name); + return *this; +} + + +/// Sets execenv jail parameters string to run the test with. +/// +/// \param params String of jail parameters. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_execenv_jail(const std::string& params) +{ + set< config::string_node >(_pimpl->props, "execenv_jail", params); + return *this; +} + + /// Sets the list of configuration variables needed by the test. /// /// \param vars Set of configuration variables. diff --git a/contrib/kyua/utils/process/executor.hpp b/contrib/kyua/utils/process/executor.hpp --- a/contrib/kyua/utils/process/executor.hpp +++ b/contrib/kyua/utils/process/executor.hpp @@ -215,6 +215,7 @@ exit_handle wait(const exec_handle); exit_handle wait_any(void); + exit_handle reap(const int); void check_interrupt(void) const; }; diff --git a/contrib/kyua/utils/process/executor.cpp b/contrib/kyua/utils/process/executor.cpp --- a/contrib/kyua/utils/process/executor.cpp +++ b/contrib/kyua/utils/process/executor.cpp @@ -671,6 +671,34 @@ data._pimpl->state_owners, all_exec_handles))); } + + executor::exit_handle + reap(const int original_pid) + { + const exec_handles_map::iterator iter = all_exec_handles.find( + original_pid); + exec_handle& data = (*iter).second; + data._pimpl->timer.unprogram(); + + if (!fs::exists(data.stdout_file())) { + std::ofstream new_stdout(data.stdout_file().c_str()); + } + if (!fs::exists(data.stderr_file())) { + std::ofstream new_stderr(data.stderr_file().c_str()); + } + + return exit_handle(std::shared_ptr< exit_handle::impl >( + new exit_handle::impl( + data.pid(), + none, + data._pimpl->unprivileged_user, + data._pimpl->start_time, datetime::timestamp::now(), + data.control_directory(), + data.stdout_file(), + data.stderr_file(), + data._pimpl->state_owners, + all_exec_handles))); + } }; @@ -853,6 +881,20 @@ } +/// Forms exit_handle for the given PID subprocess. +/// +/// Can be used in the cases when we want to do cleanup(s) of a killed test +/// subprocess, but we do not have exit handle as we usually do after normal +/// wait mechanism. +/// +/// \return A pointer to an object describing the subprocess. +executor::exit_handle +executor::executor_handle::reap(const int pid) +{ + return _pimpl->reap(pid); +} + + /// Checks if an interrupt has fired. /// /// Calls to this function should be sprinkled in strategic places through the diff --git a/contrib/kyua/utils/process/jail.hpp b/contrib/kyua/utils/process/jail.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/utils/process/jail.hpp @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file utils/process/jail.hpp +/// Collection of utilities for FreeBSD jail. + +#if !defined(UTILS_PROCESS_JAIL_HPP) +#define UTILS_PROCESS_JAIL_HPP + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/operations_fwd.hpp" + +namespace utils { +namespace process { +namespace jail { + + +void create(const utils::fs::path&, const std::string&, const std::string&); + +void exec(const utils::fs::path&, const std::string&, + const args_vector&) throw() UTILS_NORETURN; + +void remove(const utils::fs::path&, const std::string&); + + +} // namespace jail +} // namespace process +} // namespace utils + +#endif // !defined(UTILS_PROCESS_JAIL_HPP) diff --git a/contrib/kyua/utils/process/jail.cpp b/contrib/kyua/utils/process/jail.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/utils/process/jail.cpp @@ -0,0 +1,293 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "utils/process/jail.hpp" + +extern "C" { +#include +#include +} + +#include +#include +#include + +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/format/macros.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" + +namespace fs = utils::fs; +namespace process = utils::process; +namespace jail = utils::process::jail; + +using utils::process::args_vector; +using utils::process::child; + + +namespace { + + +static std::string +make_jail_name(const fs::path& program, const std::string& test_case_name) +{ + std::string name = std::regex_replace( + program.str() + "_" + test_case_name, + std::regex(R"([^A-Za-z0-9_])"), + "_"); + + const std::string::size_type limit = + 255 /* jail name max */ - 4 /* "kyua" prefix */; + if (name.length() > limit) + name.erase(0, name.length() - limit); + + return "kyua" + name; +} + + +static std::vector< std::string > +parse_jail_params_string(const std::string& str) +{ + std::vector< std::string > params; + std::string p; + char quote = 0; + + for (const char& c : str) { + // whitespace delimited parameter + if (quote == 0) { + if (std::isspace(c)) { + if (p.empty()) + continue; + params.push_back(p); + p = ""; + } + else if (c == '"' || c == '\'') { + if (!p.empty()) + params.push_back(p); + p = ""; + quote = c; + } + else + p += c; + } + + // quoted parameter + else { + if (c == quote) { + if (!p.empty()) + params.push_back(p); + p = ""; + quote = 0; + } + else + p += c; + } + } + + // leftovers + if (!p.empty()) + params.push_back(p); + + return params; +} + + +/// Functor to run a program. +class run { + /// Program binary absolute path. + const utils::fs::path& _program; + + /// Program arguments. + const args_vector& _args; + +public: + /// Constructor. + /// + /// \param program Program binary absolute path. + /// \param args Program arguments. + run( + const utils::fs::path& program, + const args_vector& args) : + _program(program), + _args(args) + { + } + + /// Body of the subprocess. + void + operator()(void) + { + process::exec(_program, _args); + } +}; + + +} // anonymous namespace + + +/// Create a jail based on test program path and case name. +/// +/// A new jail will always be 'persist', thus the caller is expected to remove +/// the jail eventually via jail::remove(). +/// +/// It's expected to be run in a subprocess. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +/// \param jail String of jail parameters. +void +jail::create(const fs::path& program, + const std::string& test_case_name, + const std::string& jail_params) +{ + args_vector av; + + // creation flag + av.push_back("-qc"); + + // jail name + av.push_back(F("name=%s") % make_jail_name(program, test_case_name)); + + // some obvious defaults to ease test authors' life + av.push_back("children.max=16"); + + // test defined jail params + const std::vector< std::string > params = parse_jail_params_string(jail_params); + for (const std::string& p : params) + av.push_back(p); + + // it must be persist + av.push_back("persist"); + + // invoke jail + std::auto_ptr< process::child > child = child::fork_capture( + run(fs::path("/usr/sbin/jail"), av)); + process::status status = child->wait(); + + // expect success + if (status.exited() && status.exitstatus() == EXIT_SUCCESS) + return; + + // otherwise, let us know what jail thinks and fail fast + char err[330]; + child->output().getline(err, 330); + std::cerr << err << "\n"; + std::exit(EXIT_FAILURE); +} + + +/// Executes an external binary in a jail and replaces the current process. +/// +/// This function must not use any of the logging features so that the output +/// of the subprocess is not "polluted" by our own messages. +/// +/// This function must also not affect the global state of the current process +/// as otherwise we would not be able to use vfork(). Only state stored in the +/// stack can be touched. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +/// \param args The arguments to pass to the binary, without the program name. +void +jail::exec(const fs::path& program, + const std::string& test_case_name, + const args_vector& args) throw() +{ + args_vector av(args); + av.insert(av.begin(), program.str()); + + // get our work dir + char cwd[256]; + if (getcwd(cwd, 256) == NULL) { + std::cerr << "process::jail::exec: getcwd() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // prepare a script to run in a jail to change back to the work dir + // and exec the program + std::string cd_exec_path = std::string(cwd) + "/cd_exec.sh"; + std::ofstream f(cd_exec_path); + if (f.fail()) { + std::cerr << "process::jail::exec: cannot create cd_exec.sh file: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + f << "#!/bin/sh\n" + << "cd \"$1\" && shift && exec $*"; + f.close(); + if (chmod(cd_exec_path.c_str(), + S_IRUSR|S_IXUSR | S_IRGRP|S_IXGRP | S_IROTH|S_IXOTH) != 0) { + std::cerr << "process::jail::exec: chmod() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // change current work dir inside a jail back to kyua work dir + av.insert(av.begin(), cwd); + av.insert(av.begin(), cd_exec_path); + + av.insert(av.begin(), make_jail_name(program, test_case_name)); + + process::exec(fs::path("/usr/sbin/jexec"), av); +} + + +/// Removes a jail based on test program path and case name. +/// +/// It's expected to be run in a subprocess. +/// +/// \param program The test program binary absolute path. +/// \param test_case_name Name of the test case. +void +jail::remove(const fs::path& program, + const std::string& test_case_name) +{ + args_vector av; + + // removal flag + av.push_back("-r"); + + // jail name + av.push_back(make_jail_name(program, test_case_name)); + + // invoke jail + std::auto_ptr< process::child > child = child::fork_capture( + run(fs::path("/usr/sbin/jail"), av)); + process::status status = child->wait(); + + // expect success + if (status.exited() && status.exitstatus() == EXIT_SUCCESS) + std::exit(EXIT_SUCCESS); + + // otherwise, let us know what jail thinks and fail fast + char err[330]; + child->output().getline(err, 330); + std::cerr << err << "\n"; + std::exit(EXIT_FAILURE); +} diff --git a/usr.bin/kyua/Makefile b/usr.bin/kyua/Makefile --- a/usr.bin/kyua/Makefile +++ b/usr.bin/kyua/Makefile @@ -89,6 +89,7 @@ utils/process/executor.cpp \ utils/process/fdstream.cpp \ utils/process/isolation.cpp \ + utils/process/jail.cpp \ utils/process/operations.cpp \ utils/process/status.cpp \ utils/process/system.cpp \ @@ -128,7 +129,9 @@ engine/scanner.cpp \ engine/tap.cpp \ engine/tap_parser.cpp \ - engine/scheduler.cpp + engine/scheduler.cpp \ + engine/execenv/execenv.cpp \ + engine/execenv/jail.cpp SRCS+= store/dbtypes.cpp \ store/exceptions.cpp \