diff --git a/Makefile.am b/Makefile.am index d7f3cd27..e721d082 100644 --- a/Makefile.am +++ b/Makefile.am @@ -59,6 +59,7 @@ include doc/Makefile.am.inc include drivers/Makefile.am.inc include engine/Makefile.am.inc include examples/Makefile.am.inc +include freebsd/Makefile.am.inc include integration/Makefile.am.inc include misc/Makefile.am.inc include model/Makefile.am.inc @@ -68,7 +69,7 @@ include utils/Makefile.am.inc bin_PROGRAMS = kyua kyua_SOURCES = main.cpp kyua_CXXFLAGS = $(CLI_CFLAGS) $(ENGINE_CFLAGS) $(UTILS_CFLAGS) -kyua_LDADD = $(CLI_LIBS) $(ENGINE_LIBS) $(UTILS_LIBS) +kyua_LDADD = $(CLI_LIBS) $(ENGINE_LIBS) $(FREEBSD_LIBS) $(UTILS_LIBS) CHECK_ENVIRONMENT = KYUA_CONFDIR="/non-existent" \ KYUA_DOCDIR="$(abs_top_srcdir)" \ diff --git a/cli/cmd_config_test.cpp b/cli/cmd_config_test.cpp index f084f99b..cd2ca65c 100644 --- a/cli/cmd_config_test.cpp +++ b/cli/cmd_config_test.cpp @@ -61,6 +61,7 @@ fake_config(void) { config::tree user_config = engine::default_config(); user_config.set_string("architecture", "the-architecture"); + user_config.set_string("execenv", "the-env"); user_config.set_string("parallelism", "128"); user_config.set_string("platform", "the-platform"); //user_config.set_string("unprivileged_user", ""); @@ -83,12 +84,13 @@ ATF_TEST_CASE_BODY(all) cmdline::ui_mock ui; ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); - ATF_REQUIRE_EQ(5, ui.out_log().size()); + ATF_REQUIRE_EQ(6, ui.out_log().size()); ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]); - ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[1]); - ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[2]); - ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[3]); - ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[4]); + ATF_REQUIRE_EQ("execenv = the-env", ui.out_log()[1]); + ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[2]); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[3]); + ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[4]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[5]); ATF_REQUIRE(ui.err_log().empty()); } diff --git a/configure.ac b/configure.ac index a0df977c..7ac37d9d 100644 --- a/configure.ac +++ b/configure.ac @@ -169,5 +169,6 @@ fi AM_CONDITIONAL(TARGET_SRCDIR_EMPTY, [test -z "${target_srcdir}"]) AC_SUBST([target_srcdir]) +AM_CONDITIONAL([FreeBSD], [test "$(uname -o)" = "FreeBSD"]) AC_OUTPUT diff --git a/doc/kyua.conf.5.in b/doc/kyua.conf.5.in index 05a9499b..2e719159 100644 --- a/doc/kyua.conf.5.in +++ b/doc/kyua.conf.5.in @@ -1,4 +1,4 @@ -.\" Copyright 2012 The Kyua Authors. +.\" Copyright 2024 The Kyua Authors. .\" All rights reserved. .\" .\" Redistribution and use in source and binary forms, with or without @@ -25,7 +25,7 @@ .\" 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. -.Dd February 20, 2015 +.Dd March 22, 2024 .Dt KYUA.CONF 5 .Os .Sh NAME @@ -36,6 +36,7 @@ .Pp Variables: .Va architecture , +.Va execenv , .Va platform , .Va test_suites , .Va unprivileged_user . @@ -72,6 +73,10 @@ The following variables are internally recognized by .Bl -tag -width XX -offset indent .It Va architecture Name of the system architecture (aka processor type). +.It Va execenv +Whitespace-separated list of execution environment names. +.Pp +Only tests which require one of the given execution environments will be run. .It Va parallelism Maximum number of test cases to execute concurrently. .It Va platform diff --git a/doc/kyuafile.5.in b/doc/kyuafile.5.in index 06cb2dbc..f218d5ec 100644 --- a/doc/kyuafile.5.in +++ b/doc/kyuafile.5.in @@ -1,4 +1,4 @@ -.\" Copyright 2012 The Kyua Authors. +.\" Copyright 2024 The Kyua Authors. .\" All rights reserved. .\" .\" Redistribution and use in source and binary forms, with or without @@ -25,7 +25,7 @@ .\" 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. -.Dd July 3, 2015 +.Dd March 22, 2024 .Dt KYUAFILE 5 .Os .Sh NAME @@ -173,6 +173,32 @@ Refer to the section below for clarification. .It Va description Textual description of the test. +.It Va execenv +The name of the execution environment to be used for running the test. If +empty or not defined, the +.Sq host +execution environment is meant. The possible values are: +.Bl -tag -width xUnnnnnnn +.It host +The default environment which runs the test as a usual child process. +.It jail +The FreeBSD jail environment. It creates a temporary jail to run the test and +its optional cleanup logic within. +.El +.It Va execenv_jail +Additional test-specific whitespace-separated parameters of FreeBSD's +.Xr jail 8 +to create a temporary jail within which the test is run. +It makes sense only if execenv is set to +.Sq jail . + +Kyua implicitly adds +.Sq children.max +.Xr jail 8 +parameter for a temporary jail with the maximum possible value according to +the jail Kyua itself is running within. It allows tests to easily spawn their +own sub-jails without additional configuration. It can be overriden via +execenv_jail if needed. .It Va is_exclusive If true, indicates that this test program cannot be executed along any other programs at the same time. @@ -360,6 +386,19 @@ test_suite('FreeBSD') plain_test_program{name='the_test', ['custom.FreeBSD-Bug-Id']='category/12345'} .Ed +.Ss FreeBSD jail execution environment +The following example configures the test to be run within a temporary jail +created by Kyua with VNET support and the permission to create raw sockets: +.Bd -literal -offset indent +syntax(2) + +test_suite('FreeBSD') + +atf_test_program{name='network_test', + execenv='jail', + execenv_jail='vnet allow.raw_sockets', + required_user='root'} +.Ed .Ss Connecting disjoint test suites Now suppose you had various test suites on your file system and you would like to connect them together so that they could be executed and treated as diff --git a/drivers/report_junit_test.cpp b/drivers/report_junit_test.cpp index 462dca72..3e933916 100644 --- a/drivers/report_junit_test.cpp +++ b/drivers/report_junit_test.cpp @@ -63,6 +63,8 @@ static const char* const default_metadata = "allowed_architectures is empty\n" "allowed_platforms is empty\n" "description is empty\n" + "execenv is empty\n" + "execenv_jail is empty\n" "has_cleanup = false\n" "is_exclusive = false\n" "required_configs is empty\n" @@ -80,6 +82,8 @@ static const char* const overriden_metadata = "allowed_architectures is empty\n" "allowed_platforms is empty\n" "description = Textual description\n" + "execenv is empty\n" + "execenv_jail is empty\n" "has_cleanup = false\n" "is_exclusive = false\n" "required_configs is empty\n" @@ -199,6 +203,8 @@ ATF_TEST_CASE_BODY(junit_metadata__overrides) .add_allowed_architecture("arch1") .add_allowed_platform("platform1") .set_description("This is a test") + .set_execenv("jail") + .set_execenv_jail("vnet") .set_has_cleanup(true) .set_is_exclusive(true) .add_required_config("config1") @@ -215,6 +221,8 @@ ATF_TEST_CASE_BODY(junit_metadata__overrides) + "allowed_architectures = arch1\n" + "allowed_platforms = platform1\n" + "description = This is a test\n" + + "execenv = jail\n" + + "execenv_jail = vnet\n" + "has_cleanup = true\n" + "is_exclusive = true\n" + "required_configs = config1\n" diff --git a/engine/Makefile.am.inc b/engine/Makefile.am.inc index baa7fe0b..68965b8a 100644 --- a/engine/Makefile.am.inc +++ b/engine/Makefile.am.inc @@ -153,3 +153,5 @@ engine_scheduler_test_SOURCES = engine/scheduler_test.cpp engine_scheduler_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) engine_scheduler_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) endif + +include engine/execenv/Makefile.am.inc diff --git a/engine/atf.cpp b/engine/atf.cpp index eb63be20..f6746dd2 100644 --- a/engine/atf.cpp +++ b/engine/atf.cpp @@ -39,6 +39,7 @@ extern "C" { #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" @@ -54,6 +55,7 @@ extern "C" { #include "utils/stream.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; @@ -190,7 +192,10 @@ engine::atf_interface::exec_test(const model::test_program& test_program, args.push_back(F("-r%s") % (control_directory / result_name)); args.push_back(test_case_name); - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } @@ -219,7 +224,9 @@ engine::atf_interface::exec_cleanup( } args.push_back(F("%s:cleanup") % test_case_name); - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->exec(args); } diff --git a/engine/atf_list.cpp b/engine/atf_list.cpp index a16b889c..1ce32f2d 100644 --- a/engine/atf_list.cpp +++ b/engine/atf_list.cpp @@ -121,6 +121,10 @@ engine::parse_atf_metadata(const model::properties_map& props) 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/engine/config.cpp b/engine/config.cpp index 3f162a94..3a4f42d6 100644 --- a/engine/config.cpp +++ b/engine/config.cpp @@ -35,6 +35,7 @@ #include #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "utils/config/exceptions.hpp" #include "utils/config/parser.hpp" #include "utils/config/tree.ipp" @@ -43,6 +44,7 @@ #include "utils/text/operations.ipp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace passwd = utils::passwd; namespace text = utils::text; @@ -59,6 +61,7 @@ static void init_tree(config::tree& tree) { tree.define< config::string_node >("architecture"); + tree.define< config::strings_set_node >("execenv"); tree.define< config::positive_int_node >("parallelism"); tree.define< config::string_node >("platform"); tree.define< engine::user_node >("unprivileged_user"); @@ -74,6 +77,14 @@ static void set_defaults(config::tree& tree) { tree.set< config::string_node >("architecture", KYUA_ARCHITECTURE); + + std::set< std::string > supported; + for (auto em : execenv::execenvs()) + if (em->is_supported()) + supported.insert(em->name()); + supported.insert("host"); + tree.set< config::strings_set_node >("execenv", supported); + // TODO(jmmv): Automatically derive this from the number of CPUs in the // machine and forcibly set to a value greater than 1. Still testing // the new parallel implementation as of 2015-02-27 though. @@ -229,6 +240,13 @@ engine::empty_config(void) { config::tree tree(false); init_tree(tree); + + // Tests of Kyua itself tend to use an empty config, and they want + // to allow running usual host based test cases. + std::set< std::string > supported; + supported.insert("host"); + tree.set< config::strings_set_node >("execenv", supported); + return tree; } diff --git a/engine/execenv/Makefile.am.inc b/engine/execenv/Makefile.am.inc new file mode 100644 index 00000000..00b11e08 --- /dev/null +++ b/engine/execenv/Makefile.am.inc @@ -0,0 +1,32 @@ +# Copyright 2024 The Kyua Authors. +# All rights reserved. +# +# 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. + +libengine_a_SOURCES += engine/execenv/execenv.hpp +libengine_a_SOURCES += engine/execenv/execenv.cpp +libengine_a_SOURCES += engine/execenv/execenv_host.hpp +libengine_a_SOURCES += engine/execenv/execenv_host.cpp diff --git a/engine/execenv/execenv.cpp b/engine/execenv/execenv.cpp new file mode 100644 index 00000000..b31116d1 --- /dev/null +++ b/engine/execenv/execenv.cpp @@ -0,0 +1,70 @@ +// 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/execenv_host.hpp" + +namespace execenv = engine::execenv; + +using utils::none; + + +/// List of registered execution environments, except default host one. +/// +/// Use register_execenv() to add an entry to this global list. +static std::vector< std::shared_ptr< execenv::manager > > + execenv_managers; + + +void +execenv::register_execenv(const std::shared_ptr< execenv::manager > manager) +{ + execenv_managers.push_back(manager); +} + + +const std::vector< std::shared_ptr< execenv::manager> > +execenv::execenvs() +{ + return execenv_managers; +} + + +std::unique_ptr< execenv::interface > +execenv::get(const model::test_program& test_program, + const std::string& test_case_name) +{ + for (auto m : execenv_managers) { + auto e = m->probe(test_program, test_case_name); + if (e != nullptr) + return e; + } + + return std::unique_ptr< execenv::interface >( + new execenv::execenv_host(test_program, test_case_name)); +} diff --git a/engine/execenv/execenv.hpp b/engine/execenv/execenv.hpp new file mode 100644 index 00000000..73d829c4 --- /dev/null +++ b/engine/execenv/execenv.hpp @@ -0,0 +1,144 @@ +// 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 subsystem interface. + +#if !defined(ENGINE_EXECENV_EXECENV_HPP) +#define ENGINE_EXECENV_EXECENV_HPP + +#include "model/test_program.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations_fwd.hpp" + +using utils::process::args_vector; +using utils::optional; + +namespace engine { +namespace execenv { + +/// Abstract interface of an execution environment. +class interface { +protected: + const model::test_program& _test_program; + const std::string& _test_case_name; + +public: + /// Constructor. + /// + /// \param program The test program. + /// \param test_case_name Name of the test case. + interface(const model::test_program& test_program, + const std::string& test_case_name) : + _test_program(test_program), + _test_case_name(test_case_name) + {} + + /// Destructor. + virtual ~interface() {} + + /// Initializes execution environment. + /// + /// It's expected to be called inside a fork which runs + /// scheduler::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. + virtual void init() const = 0; + + /// Cleanups or removes execution environment. + /// + /// It's expected to be called inside a fork for execenv cleanup. + virtual void cleanup() const = 0; + + /// Executes a test within the execution environment. + /// + /// It's expected to be called inside a fork which runs + /// scheduler::interface::exec_test() or exec_cleanup(). + /// + /// \param args The arguments to pass to the binary. + virtual void exec(const args_vector& args) const UTILS_NORETURN = 0; +}; + + +/// Abstract interface of an execution environment manager. +class manager { +public: + /// Destructor. + virtual ~manager() {} + + /// Returns name of an execution environment. + virtual const std::string& name() const = 0; + + /// Returns whether this execution environment is actually supported. + /// + /// It can be compile time and/or runtime check. + virtual bool is_supported() const = 0; + + /// Returns execution environment for a test. + /// + /// It checks if the given test is designed for this execution environment. + /// + /// \param program The test program. + /// \param test_case_name Name of the test case. + /// + /// \return An execenv object if the test conforms, or none. + virtual std::unique_ptr< interface > probe( + const model::test_program& test_program, + const std::string& test_case_name) const = 0; + + // TODO: execenv related extra metadata could be provided by a manager + // not to know how exactly and where it should be added to the kyua +}; + + +/// Registers an execution environment. +/// +/// \param manager Execution environment manager. +void register_execenv(const std::shared_ptr< manager > manager); + + +/// Returns list of registered execenv managers, except default host one. +/// +/// \return A vector of pointers to execenv managers. +const std::vector< std::shared_ptr< manager> > execenvs(); + + +/// Returns execution environment for a test case. +/// +/// \param program The test program. +/// \param test_case_name Name of the test case. +/// +/// \return An execution environment of a test. +std::unique_ptr< execenv::interface > get( + const model::test_program& test_program, + const std::string& test_case_name); + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HPP) diff --git a/engine/execenv/execenv_host.cpp b/engine/execenv/execenv_host.cpp new file mode 100644 index 00000000..dde931f8 --- /dev/null +++ b/engine/execenv/execenv_host.cpp @@ -0,0 +1,51 @@ +// Copyright (c) 2024 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_host.hpp" + +#include "utils/fs/path.hpp" +#include "utils/process/operations.hpp" + +void +execenv::execenv_host::init() const +{ + // nothing to do +} + + +void +execenv::execenv_host::cleanup() const +{ + // nothing to do +} + + +void +execenv::execenv_host::exec(const args_vector& args) const +{ + utils::process::exec(_test_program.absolute_path(), args); +} diff --git a/engine/execenv/execenv_host.hpp b/engine/execenv/execenv_host.hpp new file mode 100644 index 00000000..14815d0c --- /dev/null +++ b/engine/execenv/execenv_host.hpp @@ -0,0 +1,62 @@ +// Copyright (c) 2024 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_host.hpp +/// Default execution environment. + +#if !defined(ENGINE_EXECENV_EXECENV_HOST_HPP) +#define ENGINE_EXECENV_EXECENV_HOST_HPP + +#include "engine/execenv/execenv.hpp" + +#include "utils/process/operations_fwd.hpp" + +namespace execenv = engine::execenv; + +using utils::process::args_vector; + +namespace engine { +namespace execenv { + + +class execenv_host : public execenv::interface { +public: + execenv_host(const model::test_program& test_program, + const std::string& test_case_name) : + execenv::interface(test_program, test_case_name) + {} + + void init() const; + void cleanup() const; + void exec(const args_vector& args) const UTILS_NORETURN; +}; + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HOST_HPP) diff --git a/engine/plain.cpp b/engine/plain.cpp index 8346e50b..9a2c63f8 100644 --- a/engine/plain.cpp +++ b/engine/plain.cpp @@ -34,6 +34,7 @@ extern "C" { #include +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" @@ -47,6 +48,7 @@ extern "C" { #include "utils/sanity.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; @@ -104,7 +106,10 @@ engine::plain_interface::exec_test( } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } diff --git a/engine/requirements.cpp b/engine/requirements.cpp index a7b0a90d..f9f69985 100644 --- a/engine/requirements.cpp +++ b/engine/requirements.cpp @@ -100,6 +100,34 @@ check_allowed_architectures(const model::strings_set& allowed_architectures, } +/// Checks if test's execenv matches the user configuration. +/// +/// \param execenv Execution environment name a test is designed for. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the execenv is in the list or an error message otherwise. +static std::string +check_execenv(const std::string& execenv, const config::tree& user_config) +{ + std::string name = execenv; + if (name.empty()) + name = "host"; // if a test claims nothing then it's host based + + std::set< std::string > execenvs; + try { + execenvs = user_config.lookup< config::strings_set_node >("execenv"); + } catch (const config::unknown_key_error&) { + // okay, user config does not define it, empty set then + } + + if (execenvs.find(name) == execenvs.end()) + return F("'%s' execenv is not supported or not allowed by " + "the runtime user configuration") % name; + + return ""; +} + + /// Checks if the allowed platforms match the current architecture. /// /// \param allowed_platforms Set of allowed platforms. @@ -263,6 +291,10 @@ engine::check_reqs(const model::metadata& md, const config::tree& cfg, if (!reason.empty()) return reason; + reason = check_execenv(md.execenv(), cfg); + if (!reason.empty()) + return reason; + reason = check_allowed_platforms(md.allowed_platforms(), cfg); if (!reason.empty()) return reason; diff --git a/engine/scheduler.cpp b/engine/scheduler.cpp index e7b51d23..ee1fd306 100644 --- a/engine/scheduler.cpp +++ b/engine/scheduler.cpp @@ -40,6 +40,7 @@ extern "C" { #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" @@ -68,6 +69,7 @@ extern "C" { namespace config = utils::config; namespace datetime = utils::datetime; +namespace execenv = engine::execenv; namespace executor = utils::process::executor; namespace fs = utils::fs; namespace logging = utils::logging; @@ -87,6 +89,10 @@ using utils::optional; 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 +212,18 @@ struct test_exec_data : public exec_data { /// 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 +240,14 @@ struct test_exec_data : public exec_data { 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 +286,40 @@ struct cleanup_exec_data : public exec_data { }; +/// 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 +546,40 @@ class run_test_cleanup { }; +/// 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 */) + { + auto e = execenv::get(_test_program, _test_case_name); + e->cleanup(); + } +}; + + /// Obtains the right scheduler interface for a given test program. /// /// \param name The name of the interface of the test program. @@ -835,6 +923,22 @@ struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { % 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 +960,8 @@ struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { 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 +971,37 @@ struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { 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 +1063,61 @@ struct engine::scheduler::scheduler_handle::impl : utils::noncopyable { 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 +1307,7 @@ scheduler::scheduler_handle::spawn_test( 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 +1342,8 @@ scheduler::scheduler_handle::wait_any(void) _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 +1379,7 @@ scheduler::scheduler_handle::wait_any(void) // 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 +1404,6 @@ scheduler::scheduler_handle::wait_any(void) _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 +1412,21 @@ scheduler::scheduler_handle::wait_any(void) // 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) { + // ok, let's check for another type + } + + // 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 +1465,65 @@ scheduler::scheduler_handle::wait_any(void) _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) { + // ok, let's check for another type } + + // 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) { + // ok, it was one of the types above + } + INV(result); std::shared_ptr< result_handle::bimpl > result_handle_bimpl( diff --git a/engine/scheduler.hpp b/engine/scheduler.hpp index 24ff0b5a..ee01c83b 100644 --- a/engine/scheduler.hpp +++ b/engine/scheduler.hpp @@ -262,6 +262,7 @@ class scheduler_handle { extern utils::datetime::delta cleanup_timeout; +extern utils::datetime::delta execenv_cleanup_timeout; extern utils::datetime::delta list_timeout; diff --git a/engine/tap.cpp b/engine/tap.cpp index 85e23857..ed35ba40 100644 --- a/engine/tap.cpp +++ b/engine/tap.cpp @@ -35,6 +35,7 @@ extern "C" { #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" @@ -48,6 +49,7 @@ extern "C" { #include "utils/sanity.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; @@ -151,7 +153,10 @@ engine::tap_interface::exec_test( } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } diff --git a/examples/kyua.conf b/examples/kyua.conf index 83418a32..246eb158 100644 --- a/examples/kyua.conf +++ b/examples/kyua.conf @@ -43,6 +43,9 @@ syntax(2) -- Name of the system architecture (aka processor type). architecture = "x86_64" +-- List of execution environments. +execenv = "host jail" + -- Maximum number of jobs (such as test case runs) to execute concurrently. parallelism = 16 diff --git a/freebsd/Makefile.am.inc b/freebsd/Makefile.am.inc new file mode 100644 index 00000000..1f4ad1e7 --- /dev/null +++ b/freebsd/Makefile.am.inc @@ -0,0 +1,46 @@ +# Copyright 2024 The Kyua Authors. +# All rights reserved. +# +# 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. + +FREEBSD_CFLAGS = +FREEBSD_LIBS = libfreebsd.a + +noinst_LIBRARIES += libfreebsd.a +libfreebsd_a_CPPFLAGS = -DGDB=\"$(GDB)\" +libfreebsd_a_SOURCES = freebsd/main.hpp +libfreebsd_a_SOURCES += freebsd/main.cpp +libfreebsd_a_SOURCES += freebsd/execenv_jail.hpp +libfreebsd_a_SOURCES += freebsd/execenv_jail_manager.hpp +libfreebsd_a_SOURCES += freebsd/execenv_jail_manager.cpp + +if FreeBSD +FREEBSD_LIBS += -ljail +libfreebsd_a_SOURCES += freebsd/execenv_jail.cpp +include freebsd/utils/Makefile.am.inc +else +libfreebsd_a_SOURCES += freebsd/execenv_jail_stub.cpp +endif diff --git a/freebsd/execenv_jail.cpp b/freebsd/execenv_jail.cpp new file mode 100644 index 00000000..93a1f809 --- /dev/null +++ b/freebsd/execenv_jail.cpp @@ -0,0 +1,76 @@ +// Copyright (c) 2024 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 "freebsd/execenv_jail.hpp" + +#include "freebsd/utils/jail.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "utils/fs/path.hpp" + +using freebsd::utils::jail; + + +namespace freebsd { + + +bool execenv_jail_supported = true; + + +void +execenv_jail::init() const +{ + auto test_case = _test_program.find(_test_case_name); + + jail().create( + jail().make_name(_test_program.absolute_path(), _test_case_name), + test_case.get_metadata().execenv_jail() + ); +} + + +void +execenv_jail::cleanup() const +{ + jail().remove( + jail().make_name(_test_program.absolute_path(), _test_case_name) + ); +} + + +void +execenv_jail::exec(const args_vector& args) const +{ + jail().exec( + jail().make_name(_test_program.absolute_path(), _test_case_name), + _test_program.absolute_path(), + args + ); +} + + +} // namespace freebsd diff --git a/freebsd/execenv_jail.hpp b/freebsd/execenv_jail.hpp new file mode 100644 index 00000000..72aff5cc --- /dev/null +++ b/freebsd/execenv_jail.hpp @@ -0,0 +1,64 @@ +// Copyright (c) 2024 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 freebsd/execenv_jail.hpp +/// FreeBSD jail execution environment. + +#if !defined(FREEBSD_EXECENV_JAIL_HPP) +#define FREEBSD_EXECENV_JAIL_HPP + +#include "engine/execenv/execenv.hpp" + +#include "utils/process/operations_fwd.hpp" + +namespace execenv = engine::execenv; + +using utils::process::args_vector; + + +namespace freebsd { + + +extern bool execenv_jail_supported; + + +class execenv_jail : public execenv::interface { +public: + execenv_jail(const model::test_program& test_program, + const std::string& test_case_name) : + execenv::interface(test_program, test_case_name) + {} + + void init() const; + void cleanup() const; + void exec(const args_vector& args) const UTILS_NORETURN; +}; + + +} // namespace freebsd + +#endif // !defined(FREEBSD_EXECENV_JAIL_HPP) diff --git a/freebsd/execenv_jail_manager.cpp b/freebsd/execenv_jail_manager.cpp new file mode 100644 index 00000000..6d7119db --- /dev/null +++ b/freebsd/execenv_jail_manager.cpp @@ -0,0 +1,62 @@ +// Copyright (c) 2024 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 "freebsd/execenv_jail_manager.hpp" + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "freebsd/execenv_jail.hpp" + +static const std::string execenv_name = "jail"; + +const std::string& +freebsd::execenv_jail_manager::name() const +{ + return execenv_name; +} + + +bool +freebsd::execenv_jail_manager::is_supported() const +{ + return freebsd::execenv_jail_supported; +} + + +std::unique_ptr< execenv::interface > +freebsd::execenv_jail_manager::probe( + const model::test_program& test_program, + const std::string& test_case_name) const +{ + auto test_case = test_program.find(test_case_name); + if (test_case.get_metadata().execenv() != execenv_name) + return nullptr; + + return std::unique_ptr< execenv::interface >( + new freebsd::execenv_jail(test_program, test_case_name) + ); +} diff --git a/freebsd/execenv_jail_manager.hpp b/freebsd/execenv_jail_manager.hpp new file mode 100644 index 00000000..99f32187 --- /dev/null +++ b/freebsd/execenv_jail_manager.hpp @@ -0,0 +1,53 @@ +// Copyright (c) 2024 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 freebsd/execenv_jail_manager.hpp +/// FreeBSD jail execution environment manager. + +#if !defined(FREEBSD_EXECENV_JAIL_MANAGER_HPP) +#define FREEBSD_EXECENV_JAIL_MANAGER_HPP + +#include "engine/execenv/execenv.hpp" + +namespace execenv = engine::execenv; + +namespace freebsd { + + +class execenv_jail_manager : public execenv::manager { +public: + const std::string& name() const; + bool is_supported() const; + std::unique_ptr< execenv::interface > probe( + const model::test_program& test_program, + const std::string& test_case_name) const; +}; + + +} // namespace freebsd + +#endif // !defined(FREEBSD_EXECENV_JAIL_MANAGER_HPP) diff --git a/freebsd/execenv_jail_stub.cpp b/freebsd/execenv_jail_stub.cpp new file mode 100644 index 00000000..d14fa604 --- /dev/null +++ b/freebsd/execenv_jail_stub.cpp @@ -0,0 +1,74 @@ +// Copyright (c) 2024 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 "freebsd/execenv_jail.hpp" + +#include + +#include "utils/process/operations_fwd.hpp" + +using utils::process::args_vector; + + +static inline void requires_freebsd(void) UTILS_NORETURN; + +static inline void +requires_freebsd(void) +{ + std::cerr << "execenv=\"jail\" requires FreeBSD with jail feature.\n"; + std::exit(EXIT_FAILURE); +} + + +namespace freebsd { + + +bool execenv_jail_supported = false; + + +void +execenv_jail::init() const +{ + requires_freebsd(); +} + + +void +execenv_jail::cleanup() const +{ + requires_freebsd(); +} + + +void +execenv_jail::exec(const args_vector&) const +{ + requires_freebsd(); +} + + +} // namespace freebsd diff --git a/freebsd/main.cpp b/freebsd/main.cpp new file mode 100644 index 00000000..9b242e21 --- /dev/null +++ b/freebsd/main.cpp @@ -0,0 +1,53 @@ +// Copyright (c) 2024 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 "freebsd/main.hpp" + +#include "engine/execenv/execenv.hpp" +#include "freebsd/execenv_jail_manager.hpp" + +namespace execenv = engine::execenv; + +/// FreeBSD related features initialization. +/// +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This throws any uncaught exception. Such exceptions +/// are bugs, but we let them propagate so that the runtime will abort and +/// dump core. +int +freebsd::main(const int, const char* const* const) +{ + execenv::register_execenv( + std::shared_ptr< execenv::manager >(new freebsd::execenv_jail_manager()) + ); + + return 0; +} diff --git a/freebsd/main.hpp b/freebsd/main.hpp new file mode 100644 index 00000000..56fe1b30 --- /dev/null +++ b/freebsd/main.hpp @@ -0,0 +1,40 @@ +// Copyright (c) 2024 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 freebsd/main.hpp +/// FreeBSD related features initialization. + +#if !defined(FREEBSD_MAIN_HPP) +#define FREEBSD_MAIN_HPP + +namespace freebsd { + +int main(const int argc, const char* const* const argv); + +} // namespace freebsd + +#endif // !defined(FREEBSD_MAIN_HPP) diff --git a/freebsd/utils/Makefile.am.inc b/freebsd/utils/Makefile.am.inc new file mode 100644 index 00000000..9641c711 --- /dev/null +++ b/freebsd/utils/Makefile.am.inc @@ -0,0 +1,30 @@ +# Copyright 2024 The Kyua Authors. +# All rights reserved. +# +# 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. + +libfreebsd_a_SOURCES += freebsd/utils/jail.hpp +libfreebsd_a_SOURCES += freebsd/utils/jail.cpp diff --git a/freebsd/utils/jail.cpp b/freebsd/utils/jail.cpp new file mode 100644 index 00000000..182ea727 --- /dev/null +++ b/freebsd/utils/jail.cpp @@ -0,0 +1,323 @@ +// Copyright (c) 2024 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 "freebsd/utils/jail.hpp" + +extern "C" { +#include +#include +#include + +// FreeBSD sysctl facility +#include + +// FreeBSD Jail syscalls +#include +#include + +// FreeBSD Jail library +#include +} + +#include +#include +#include + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#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 process = utils::process; +namespace fs = utils::fs; + +using utils::process::args_vector; +using utils::process::child; + + +static const int jail_name_max_len = MAXHOSTNAMELEN - 1; +static const char* jail_name_prefix = "kyua"; + + +/// 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); + } +}; + + +namespace freebsd { +namespace utils { + + +std::vector< std::string > +jail::parse_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; +} + + +/// Constructs a jail name based on program and test case. +/// +/// The formula is "kyua" + + "_" + . +/// All non-alphanumeric chars are replaced with "_". +/// +/// If a resulting string exceeds maximum allowed length of a jail name, +/// then it's shortened from the left side keeping the "kyua" prefix. +/// +/// \param program The test program. +/// \param test_case_name Name of the test case. +/// +/// \return A jail name string. +std::string +jail::make_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 = + jail_name_max_len - strlen(jail_name_prefix); + if (name.length() > limit) + name.erase(0, name.length() - limit); + + return jail_name_prefix + name; +} + + +/// Create a jail with a given name and params string. +/// +/// A new jail will always be 'persist', thus the caller is expected to remove +/// the jail eventually via remove(). +/// +/// It's expected to be run in a subprocess. +/// +/// \param jail_name Name of a new jail. +/// \param jail_params String of jail parameters. +void +jail::create(const std::string& jail_name, + const std::string& jail_params) +{ + args_vector av; + + // creation flag + av.push_back("-qc"); + + // jail name + av.push_back("name=" + jail_name); + + // determine maximum allowed children.max + int max; + size_t len = sizeof(max); + if (::sysctlbyname("security.jail.children.max", &max, &len, NULL, 0) != 0) { + std::cerr << "sysctlbyname(security.jail.children.max) errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + if (len < sizeof(max)) { + std::cerr << "sysctlbyname(security.jail.children.max) provides less " + "data (" << len << ") than expected (" << sizeof(max) << ").\n"; + std::exit(EXIT_FAILURE); + } + if (max < 0) { + std::cerr << "sysctlbyname(security.jail.children.max) yields " + "abnormal " << max << ".\n"; + std::exit(EXIT_FAILURE); + } + if (max > 0) + max--; // a child jail must have less than parent's children.max + av.push_back("children.max=" + std::to_string(max)); + + // test defined jail params + const std::vector< std::string > params = parse_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 + std::cerr << child->output().rdbuf(); + std::exit(EXIT_FAILURE); +} + + +/// Executes an external binary in a jail and replaces the current process. +/// +/// \param jail_name Name of the jail to run within. +/// \param program The test program binary absolute path. +/// \param args The arguments to pass to the binary, without the program name. +void +jail::exec(const std::string& jail_name, + const fs::path& program, + const args_vector& args) throw() +{ + // get work dir prepared by kyua + char cwd[PATH_MAX]; + if (::getcwd(cwd, sizeof(cwd)) == NULL) { + std::cerr << "process::jail::exec: getcwd() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // get jail id by its name + int jid = ::jail_getid(jail_name.c_str()); + if (jid < 0) { + std::cerr << "process::jail::exec: jail_getid() errors: " + << strerror(errno) << ": " << jail_errmsg << ".\n"; + std::exit(EXIT_FAILURE); + } + + // attach to the jail + if (::jail_attach(jid) == -1) { + std::cerr << "process::jail::exec: jail_attach() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // set back the expected work dir + if (::chdir(cwd) == -1) { + std::cerr << "process::jail::exec: chdir() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + process::exec(program, args); +} + + +/// Removes a jail with a given name. +/// +/// It's expected to be run in a subprocess. +/// +/// \param jail_name Name of a jail to remove. +void +jail::remove(const std::string& jail_name) +{ + args_vector av; + + // removal flag + av.push_back("-r"); + + // jail name + av.push_back(jail_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 + std::cerr << child->output().rdbuf(); + std::exit(EXIT_FAILURE); +} + + +} // namespace utils +} // namespace freebsd diff --git a/freebsd/utils/jail.hpp b/freebsd/utils/jail.hpp new file mode 100644 index 00000000..262efe94 --- /dev/null +++ b/freebsd/utils/jail.hpp @@ -0,0 +1,63 @@ +// Copyright (c) 2024 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 freebsd/utils/jail.hpp +/// FreeBSD jail utilities. + +#if !defined(FREEBSD_UTILS_JAIL_HPP) +#define FREEBSD_UTILS_JAIL_HPP + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/operations_fwd.hpp" + +namespace fs = utils::fs; + +using utils::process::args_vector; + +namespace freebsd { +namespace utils { + + +class jail { +public: + std::vector< std::string > parse_params_string(const std::string& str); + std::string make_name(const fs::path& program, + const std::string& test_case_name); + void create(const std::string& jail_name, + const std::string& jail_params); + void exec(const std::string& jail_name, + const fs::path& program, + const args_vector& args) throw() UTILS_NORETURN; + void remove(const std::string& jail_name); +}; + + +} // namespace utils +} // namespace freebsd + +#endif // !defined(FREEBSD_UTILS_JAIL_HPP) diff --git a/integration/cmd_config_test.sh b/integration/cmd_config_test.sh index ed457e5c..09243002 100644 --- a/integration/cmd_config_test.sh +++ b/integration/cmd_config_test.sh @@ -42,6 +42,7 @@ all_body() { cat >"${HOME}/.kyua/kyua.conf" <expout <("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 @@ set_defaults(config::tree& tree) 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)); @@ -493,6 +497,45 @@ model::metadata::is_exclusive(void) const } +/// 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 the list of configuration variables needed by the test. /// /// \return Set of configuration variables. @@ -920,6 +963,36 @@ model::metadata_builder::set_is_exclusive(const bool exclusive) } +/// 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/model/metadata.hpp b/model/metadata.hpp index c7dd4519..28098c2a 100644 --- a/model/metadata.hpp +++ b/model/metadata.hpp @@ -69,6 +69,9 @@ class metadata { 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; 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 +115,8 @@ class metadata_builder : utils::noncopyable { 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/model/metadata_test.cpp b/model/metadata_test.cpp index 7b22653e..0a41c791 100644 --- a/model/metadata_test.cpp +++ b/model/metadata_test.cpp @@ -315,6 +315,8 @@ ATF_TEST_CASE_BODY(to_properties) props["allowed_platforms"] = ""; props["custom.foo"] = "bar"; props["description"] = ""; + props["execenv"] = ""; + props["execenv_jail"] = ""; props["has_cleanup"] = "false"; props["is_exclusive"] = "false"; props["required_configs"] = ""; @@ -406,7 +408,8 @@ ATF_TEST_CASE_BODY(output__defaults) std::ostringstream str; str << model::metadata_builder().build(); ATF_REQUIRE_EQ("metadata{allowed_architectures='', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', " "required_disk_space='0', required_files='', " "required_memory='0', " @@ -428,7 +431,8 @@ ATF_TEST_CASE_BODY(output__some_values) .build(); ATF_REQUIRE_EQ( "metadata{allowed_architectures='abc', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='true', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='true', " "required_configs='', " "required_disk_space='0', required_files='bar foo', " "required_memory='1.00K', " diff --git a/model/test_case_test.cpp b/model/test_case_test.cpp index 1a55de0f..b735f982 100644 --- a/model/test_case_test.cpp +++ b/model/test_case_test.cpp @@ -200,7 +200,8 @@ ATF_TEST_CASE_BODY(test_case__output) ATF_REQUIRE_EQ( "test_case{name='the-name', " "metadata=metadata{allowed_architectures='', allowed_platforms='foo', " - "custom.bar='baz', description='', has_cleanup='false', " + "custom.bar='baz', description='', execenv='', execenv_jail='', " + "has_cleanup='false', " "is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " diff --git a/model/test_program_test.cpp b/model/test_program_test.cpp index f9a8f7e5..1309d2af 100644 --- a/model/test_program_test.cpp +++ b/model/test_program_test.cpp @@ -544,7 +544,8 @@ check_output__no_test_cases(void) "test_program{interface='plain', binary='binary/path', " "root='/the/root', test_suite='suite-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}, " @@ -593,21 +594,23 @@ check_output__some_test_cases(void) "test_program{interface='plain', binary='binary/path', " "root='/the/root', test_suite='suite-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}, " "test_cases=map(" "another-name=test_case{name='another-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}}, " "the-name=test_case{name='the-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='foo', " - "custom.bar='baz', description='', has_cleanup='false', " - "is_exclusive='false', " + "custom.bar='baz', description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}})}", diff --git a/utils/config/nodes.ipp b/utils/config/nodes.ipp index 9e0a1228..0ec3832c 100644 --- a/utils/config/nodes.ipp +++ b/utils/config/nodes.ipp @@ -382,9 +382,14 @@ config::base_set_node< ValueType >::push_lua(lutok::state& /* state */) const template< typename ValueType > void config::base_set_node< ValueType >::set_lua( - lutok::state& /* state */, - const int /* value_index */) + lutok::state& state, + const int value_index) { + if (state.is_string(value_index)) { + set_string(state.to_string(value_index)); + return; + } + UNREACHABLE; } diff --git a/utils/process/executor.cpp b/utils/process/executor.cpp index a0063261..7e18837a 100644 --- a/utils/process/executor.cpp +++ b/utils/process/executor.cpp @@ -689,6 +689,34 @@ struct utils::process::executor::executor_handle::impl : utils::noncopyable { 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))); + } }; @@ -879,6 +907,20 @@ executor::executor_handle::wait_any(void) } +/// 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/utils/process/executor.hpp b/utils/process/executor.hpp index 858ad9c8..1b5faf51 100644 --- a/utils/process/executor.hpp +++ b/utils/process/executor.hpp @@ -215,6 +215,7 @@ class executor_handle { exit_handle wait(const exec_handle); exit_handle wait_any(void); + exit_handle reap(const int); void check_interrupt(void) const; };