Skip to content

Commit

Permalink
Add FreeBSD Jail execution environment support
Browse files Browse the repository at this point in the history
Two new test case metadata are introduced:
- execenv: it can be "host" by default or "jail"
- execenv_jail: additional jail creation parameters to jail(8)

Kyua implicitly adds "children.max" to execenv_jail parameters with the
maximum possible value. A test case can override it.

If a test is marked with is_exclusive="true" and execenv="jail" then exclusive
flag will be ignored and such test will be run in parallel if possible.

A test case lifecycle is extended:
- execenv init (creates a jail or does nothing for default execenv="host")
- test exec
- cleanup exec (optional)
- execenv cleanup (removes a jail or does nothing for default execenv="host")
  • Loading branch information
ihoro committed Feb 3, 2024
1 parent c85354e commit 70e41f0
Show file tree
Hide file tree
Showing 16 changed files with 1,118 additions and 9 deletions.
8 changes: 6 additions & 2 deletions engine/atf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -190,7 +191,9 @@ 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);

engine::execenv::init(test_program, test_case_name);
engine::execenv::exec(test_program, test_case_name, args);
}


Expand Down Expand Up @@ -219,7 +222,8 @@ engine::atf_interface::exec_cleanup(
}

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);
}


Expand Down
4 changes: 4 additions & 0 deletions engine/atf_list.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
106 changes: 106 additions & 0 deletions engine/execenv/execenv.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro>
//
// 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 {
// 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 {
// 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 {
// cleanup is not expected to be called for host environment
std::exit(EXIT_SUCCESS);
}
}
59 changes: 59 additions & 0 deletions engine/execenv/execenv.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro>
//
// 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)
138 changes: 138 additions & 0 deletions engine/execenv/jail.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro>
//
// 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"

extern "C" {
// FreeBSD Jail
#include <sys/param.h>
}

#include <regex>

#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;
namespace fs = utils::fs;

using utils::process::args_vector;


namespace {


static const int jail_name_max_len = MAXHOSTNAMELEN - 1;
static const char* jail_name_prefix = "kyua";

/// Constructs a jail name based on program and test case.
///
/// The formula is "kyua" + <program path> + "_" + <test case name>.
/// 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.
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 =
jail_name_max_len - strlen(jail_name_prefix);
if (name.length() > limit)
name.erase(0, name.length() - limit);

return jail_name_prefix + name;
}


} // anonymous namespace


/// 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.
/// \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(
make_jail_name(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.
/// \param test_case_name Name of the test case.
/// \param args The arguments to pass to the binary, without the program name.
void
execenv::jail::exec(const model::test_program& test_program,
const std::string& test_case_name,
const args_vector& args) throw()
{
process::jail::exec(
make_jail_name(test_program.absolute_path(), test_case_name),
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.
/// \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(
make_jail_name(test_program.absolute_path(), test_case_name));
}
Loading

0 comments on commit 70e41f0

Please sign in to comment.