blob: cb1e229f7f40aa71f57417c33fccb2cfb313a1f5 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <memory>
#include <set>
#include <string>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
#include <process/future.hpp>
#include <process/reap.hpp>
#include <process/gmock.hpp>
#include <process/gtest.hpp>
#include <stout/bytes.hpp>
#include <stout/exit.hpp>
#include <stout/foreach.hpp>
#include <stout/gtest.hpp>
#include <stout/os.hpp>
#include <stout/set.hpp>
#include <stout/tests/utils.hpp>
#include <stout/try.hpp>
#include "linux/cgroups2.hpp"
#include "linux/ebpf.hpp"
#include "tests/containerizer/memory_test_helper.hpp"
using process::Future;
using std::pair;
using std::set;
using std::string;
using std::thread;
using std::tuple;
using std::unique_ptr;
using std::vector;
namespace cpu = cgroups2::cpu;
namespace devices = cgroups2::devices;
namespace mesos {
namespace internal {
namespace tests {
const string TEST_CGROUP = "test";
// Cgroups2 test fixture that ensures `TEST_CGROUP` does not
// exist before and after the test is run.
class Cgroups2Test : public TemporaryDirectoryTest
{
protected:
Try<Nothing> enable_controllers(const vector<string>& controllers)
{
Try<set<string>> enabled = cgroups2::controllers::enabled(
cgroups2::ROOT_CGROUP);
if (enabled.isError()) {
return Error("Failed to check enabled controllers: " + enabled.error());
}
set<string> to_enable(controllers.begin(), controllers.end());
to_enable = to_enable - *enabled;
Try<Nothing> result = cgroups2::controllers::enable(
cgroups2::ROOT_CGROUP,
to_enable);
if (result.isSome()) {
enabled_controllers = enabled_controllers | to_enable;
}
return result;
}
void SetUp() override
{
TemporaryDirectoryTest::SetUp();
// Cleanup the test cgroup, in case a previous test run didn't clean it
// up properly.
if (cgroups2::exists(TEST_CGROUP)) {
AWAIT_READY(cgroups2::destroy(TEST_CGROUP));
}
}
void TearDown() override
{
if (cgroups2::exists(TEST_CGROUP)) {
AWAIT_READY(cgroups2::destroy(TEST_CGROUP));
}
ASSERT_SOME(cgroups2::controllers::disable(
cgroups2::ROOT_CGROUP, enabled_controllers));
TemporaryDirectoryTest::TearDown();
}
// These are controllers that *we* enabled, and therefore we need to
// disable on test cleanup. Note that if the tests are killed, then
// we'll still of course leak these side effects, unfortunately.
set<string> enabled_controllers;
};
TEST_F(Cgroups2Test, ROOT_CGROUPS2_Enabled)
{
EXPECT_TRUE(cgroups2::enabled());
}
TEST_F(Cgroups2Test, CGROUPS2_Path)
{
EXPECT_EQ("/sys/fs/cgroup/", cgroups2::path(cgroups2::ROOT_CGROUP));
EXPECT_EQ("/sys/fs/cgroup/foo", cgroups2::path("foo"));
EXPECT_EQ("/sys/fs/cgroup/foo/bar", cgroups2::path("foo/bar"));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_AvailableSubsystems)
{
Try<bool> mounted = cgroups2::mounted();
ASSERT_SOME(mounted);
if (!*mounted) {
ASSERT_SOME(cgroups2::mount());
}
Try<set<string>> available = cgroups2::controllers::available(
cgroups2::ROOT_CGROUP);
ASSERT_SOME(available);
EXPECT_TRUE(available->count("cpu") == 1);
if (!*mounted) {
EXPECT_SOME(cgroups2::unmount());
}
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_AssignProcesses)
{
Try<set<pid_t>> pids = cgroups2::processes(cgroups2::ROOT_CGROUP);
EXPECT_SOME(pids);
EXPECT_TRUE(pids->size() > 0);
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
pid_t pid = ::fork();
ASSERT_NE(-1, pid);
if (pid == 0) {
// In child process, wait for kill signal.
while (true) { sleep(1); }
SAFE_EXIT(
EXIT_FAILURE, "Error, child should be killed before reaching here");
}
pids = cgroups2::processes(TEST_CGROUP);
EXPECT_SOME(pids);
EXPECT_EQ(0u, pids->size());
EXPECT_SOME(cgroups2::assign(TEST_CGROUP, pid));
pids = cgroups2::processes(TEST_CGROUP);
EXPECT_SOME(pids);
EXPECT_EQ(1u, pids->size());
EXPECT_EQ(pid, *pids->begin());
// Should fetch the `pid` from the nested `TEST_CGROUP` if `recursive=true`.
Try<set<pid_t>> root_pids = cgroups2::processes(cgroups2::ROOT_CGROUP, true);
EXPECT_SOME(pids);
EXPECT_EQ(1u, root_pids->count(pid));
// Kill the child process.
ASSERT_NE(-1, ::kill(pid, SIGKILL));
AWAIT_EXPECT_WTERMSIG_EQ(SIGKILL, process::reap(pid));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_Threads)
{
const size_t NUM_THREADS = 5;
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
pid_t pid = ::fork();
ASSERT_NE(-1, pid);
if (pid == 0) {
vector<unique_ptr<std::thread>> runningThreads(NUM_THREADS);
for (size_t i = 0; i < NUM_THREADS; ++i) {
runningThreads[i] = unique_ptr<std::thread>(new std::thread([]() {
os::sleep(Seconds(10));
}));
}
// Check the test cgroup is initially empty.
Try<set<pid_t>> cgroupThreads = cgroups2::threads(TEST_CGROUP);
if (cgroupThreads.isError()) {
SAFE_EXIT(EXIT_FAILURE, "Failed to read threads in the cgroup");
}
if (!cgroupThreads->empty()) {
SAFE_EXIT(EXIT_FAILURE, "Expected no threads in the cgroup");
}
// Assign ourselves to the test cgroup.
if (cgroups2::assign(TEST_CGROUP, ::getpid()).isError()) {
SAFE_EXIT(EXIT_FAILURE, "Failed to assign to the test cgroup");
}
Try<set<pid_t>> threads = proc::threads(::getpid());
if (threads.isError()) {
SAFE_EXIT(EXIT_FAILURE, "Failed to read threads");
}
// Check that the test cgroup now only contains all child threads.
cgroupThreads = cgroups2::threads(TEST_CGROUP);
if (cgroupThreads.isError()) {
SAFE_EXIT(EXIT_FAILURE, "Failed to read threads in the cgroup");
}
if (*threads != *cgroupThreads) {
SAFE_EXIT(
EXIT_FAILURE,
"The threads in the cgroup did not match the processes's threads");
}
::_exit(EXIT_SUCCESS);
}
AWAIT_EXPECT_WEXITSTATUS_EQ(EXIT_SUCCESS, process::reap(pid));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_CpuStats)
{
ASSERT_SOME(enable_controllers({"cpu"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"cpu"}));
ASSERT_SOME(cgroups2::cpu::stats(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_EnableAndDisable)
{
ASSERT_SOME(enable_controllers({"cpu"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
// Check that "cpu" not enabled.
Try<set<string>> enabled = cgroups2::controllers::enabled(TEST_CGROUP);
EXPECT_SOME(enabled);
EXPECT_EQ(0u, enabled->count("cpu"));
// Enable "cpu".
EXPECT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"cpu"}));
EXPECT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"cpu"})); // NOP
// Check that "cpu" is enabled.
enabled = cgroups2::controllers::enabled(TEST_CGROUP);
EXPECT_SOME(enabled);
EXPECT_EQ(1u, enabled->count("cpu"));
// Disable "cpu".
EXPECT_SOME(cgroups2::controllers::disable(TEST_CGROUP, {"cpu"}));
EXPECT_SOME(cgroups2::controllers::disable(TEST_CGROUP, {"cpu"})); // NOP
// Check that "cpu" not enabled.
enabled = cgroups2::controllers::enabled(TEST_CGROUP);
EXPECT_SOME(enabled);
EXPECT_EQ(0u, enabled->count("cpu"));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_CpuBandwidthLimit)
{
ASSERT_SOME(enable_controllers({"cpu"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"cpu"}));
Try<cpu::BandwidthLimit> limit = cgroups2::cpu::max(TEST_CGROUP);
ASSERT_SOME(limit);
EXPECT_NONE(limit->limit); // The default limit is limitless.
cpu::BandwidthLimit new_limit =
cpu::BandwidthLimit(Microseconds(10000), Microseconds(20000));
EXPECT_SOME(cgroups2::cpu::set_max(TEST_CGROUP, new_limit));
limit = cgroups2::cpu::max(TEST_CGROUP);
ASSERT_SOME(limit);
EXPECT_EQ(new_limit.limit, limit->limit);
EXPECT_EQ(new_limit.period, limit->period);
new_limit = cpu::BandwidthLimit();
EXPECT_SOME(cgroups2::cpu::set_max(TEST_CGROUP, new_limit));
limit = cgroups2::cpu::max(TEST_CGROUP);
ASSERT_SOME(limit);
EXPECT_EQ(new_limit.limit, limit->limit);
EXPECT_EQ(new_limit.period, limit->period);
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemoryUsage)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
// Does not exist for the root cgroup.
EXPECT_ERROR(cgroups2::memory::usage(cgroups2::ROOT_CGROUP));
EXPECT_SOME(cgroups2::memory::usage(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemoryStats)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
ASSERT_SOME(cgroups2::memory::stats(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemoryLow)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
const Bytes bytes = Bytes(os::pagesize()) * 5;
// Does not exist for the root cgroup.
EXPECT_ERROR(cgroups2::memory::low(cgroups2::ROOT_CGROUP));
EXPECT_ERROR(cgroups2::memory::set_low(cgroups2::ROOT_CGROUP, bytes));
EXPECT_SOME(cgroups2::memory::set_low(TEST_CGROUP, bytes));
EXPECT_SOME_EQ(bytes, cgroups2::memory::low(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemoryMinimum)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
const Bytes bytes = Bytes(os::pagesize()) * 5;
// Does not exist for the root cgroup.
EXPECT_ERROR(cgroups2::memory::min(cgroups2::ROOT_CGROUP));
EXPECT_ERROR(cgroups2::memory::set_min(cgroups2::ROOT_CGROUP, bytes));
EXPECT_SOME(cgroups2::memory::set_min(TEST_CGROUP, bytes));
EXPECT_SOME_EQ(bytes, cgroups2::memory::min(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemoryMaximum)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
Bytes limit = Bytes(os::pagesize()) * 5;
// Does not exist for the root cgroup.
EXPECT_ERROR(cgroups2::memory::max(cgroups2::ROOT_CGROUP));
EXPECT_ERROR(cgroups2::memory::set_max(cgroups2::ROOT_CGROUP, limit));
EXPECT_SOME(cgroups2::memory::set_max(TEST_CGROUP, limit));
EXPECT_SOME_EQ(limit, cgroups2::memory::max(TEST_CGROUP));
EXPECT_SOME(cgroups2::memory::set_max(TEST_CGROUP, None()));
EXPECT_NONE(cgroups2::memory::max(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemorySoftMaximum)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
Bytes limit = Bytes(os::pagesize()) * 5;
// Does not exist for the root cgroup.
EXPECT_ERROR(cgroups2::memory::high(cgroups2::ROOT_CGROUP));
EXPECT_ERROR(cgroups2::memory::set_high(cgroups2::ROOT_CGROUP, limit));
EXPECT_SOME(cgroups2::memory::set_high(TEST_CGROUP, limit));
EXPECT_SOME_EQ(limit, cgroups2::memory::high(TEST_CGROUP));
EXPECT_SOME(cgroups2::memory::set_high(TEST_CGROUP, None()));
EXPECT_NONE(cgroups2::memory::high(TEST_CGROUP));
}
// Check that byte amounts written to the memory controller are rounded
// down to the nearest page size.
TEST_F(Cgroups2Test, ROOT_CGROUPS2_MemoryBytesRounding)
{
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
const Bytes bytes = Bytes(os::pagesize());
EXPECT_SOME(cgroups2::memory::set_min(TEST_CGROUP, bytes - 1));
EXPECT_SOME_EQ(Bytes(0), cgroups2::memory::min(TEST_CGROUP));
EXPECT_SOME(cgroups2::memory::set_min(TEST_CGROUP, bytes + 1));
EXPECT_SOME_EQ(bytes, cgroups2::memory::min(TEST_CGROUP));
EXPECT_SOME(cgroups2::memory::set_min(TEST_CGROUP, bytes * 5 - 1));
EXPECT_SOME_EQ(bytes * 4, cgroups2::memory::min(TEST_CGROUP));
EXPECT_SOME(cgroups2::memory::set_min(TEST_CGROUP, bytes * 5));
EXPECT_SOME_EQ(bytes * 5, cgroups2::memory::min(TEST_CGROUP));
EXPECT_SOME(cgroups2::memory::set_min(TEST_CGROUP, bytes * 5 + 1));
EXPECT_SOME_EQ(bytes * 5, cgroups2::memory::min(TEST_CGROUP));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_GetCgroups)
{
vector<string> cgroups = {
path::join(TEST_CGROUP, "test1"),
path::join(TEST_CGROUP, "test1/a"),
path::join(TEST_CGROUP, "test1/b"),
path::join(TEST_CGROUP, "test1/b/c"),
path::join(TEST_CGROUP, "test2"),
path::join(TEST_CGROUP, "test2/a"),
path::join(TEST_CGROUP, "test2/a/b"),
path::join(TEST_CGROUP, "test3"),
};
foreach (const string& cgroup, cgroups) {
ASSERT_SOME(cgroups2::create(cgroup, true));
}
EXPECT_SOME_EQ(
set<string>({
path::join(TEST_CGROUP, "test1"),
path::join(TEST_CGROUP, "test1/a"),
path::join(TEST_CGROUP, "test1/b"),
path::join(TEST_CGROUP, "test1/b/c"),
path::join(TEST_CGROUP, "test2"),
path::join(TEST_CGROUP, "test2/a"),
path::join(TEST_CGROUP, "test2/a/b"),
path::join(TEST_CGROUP, "test3"),
}),
cgroups2::get(TEST_CGROUP));
EXPECT_SOME_EQ(
set<string>({
path::join(TEST_CGROUP, "test1/a"),
path::join(TEST_CGROUP, "test1/b"),
path::join(TEST_CGROUP, "test1/b/c"),
}),
cgroups2::get(path::join(TEST_CGROUP, "test1")));
EXPECT_SOME_EQ(set<string>({
path::join(TEST_CGROUP, "test2/a"),
path::join(TEST_CGROUP, "test2/a/b"),
}),
cgroups2::get(path::join(TEST_CGROUP, "test2")));
EXPECT_SOME_EQ(
set<string>({
path::join(TEST_CGROUP, "test1/b/c")
}),
cgroups2::get(path::join(TEST_CGROUP, "test1/b")));
}
TEST_F(Cgroups2Test, ROOT_CGROUPS2_OomDetection)
{
// Check that exceeding the hard memory limit will trigger an OOM event.
//
// We set a hard memory limit on the test cgroup, move a process inside of
// the test cgroup subtree, listen for an OOM event, deliberately trigger an
// OOM event by exceeding the hard limit, and then check that an event was
// triggered.
ASSERT_SOME(enable_controllers({"memory"}));
ASSERT_SOME(cgroups2::create(TEST_CGROUP));
ASSERT_SOME(cgroups2::controllers::enable(TEST_CGROUP, {"memory"}));
const string leaf_cgroup = TEST_CGROUP + "/leaf";
ASSERT_SOME(cgroups2::create(leaf_cgroup));
MemoryTestHelper helper;
ASSERT_SOME(helper.spawn());
ASSERT_SOME(helper.pid());
Bytes limit = Megabytes(64);
ASSERT_SOME(cgroups2::assign(leaf_cgroup, *helper.pid()));
ASSERT_SOME(cgroups2::memory::set_max(TEST_CGROUP, limit));
Future<Nothing> oomEvent = cgroups2::memory::oom(TEST_CGROUP);
// Assert that the OOM event has not been triggered.
EXPECT_FALSE(oomEvent.isReady());
// Increase memory usage beyond the limit.
ASSERT_ERROR(helper.increaseRSS(limit * 2));
AWAIT_EXPECT_READY(oomEvent);
}
// Arguments for os::open(). Combination of a path and an access type.
typedef pair<string, int> OpenArgs;
using DeviceControllerTestParams = tuple<
vector<devices::Entry>,
vector<devices::Entry>,
vector<OpenArgs>,
vector<OpenArgs>>;
class DeviceControllerTestFixture :
public Cgroups2Test,
public ::testing::WithParamInterface<DeviceControllerTestParams> {};
TEST_P(DeviceControllerTestFixture, ROOT_CGROUPS2_DeviceController)
{
const string& cgroup = TEST_CGROUP;
auto params = GetParam();
const vector<devices::Entry>& allow = std::get<0>(params);
const vector<devices::Entry>& deny = std::get<1>(params);
const vector<OpenArgs>& allowed_accesses = std::get<2>(params);
const vector<OpenArgs>& blocked_accesses = std::get<3>(params);
ASSERT_SOME(cgroups2::create(cgroup));
string path = cgroups2::path(cgroup);
Try<vector<uint32_t>> attached = ebpf::cgroups2::attached(path);
ASSERT_SOME(attached);
ASSERT_EQ(0u, attached->size());
ASSERT_SOME(devices::configure(cgroup, allow, deny));
attached = ebpf::cgroups2::attached(path);
ASSERT_SOME(attached);
ASSERT_EQ(1u, attached->size());
pid_t pid = ::fork();
ASSERT_NE(-1, pid);
if (pid == 0) {
// Move the child process into the newly created cgroup.
Try<Nothing> assign = cgroups2::assign(cgroup, ::getpid());
if (assign.isError()) {
SAFE_EXIT(EXIT_FAILURE, "Failed to assign child process to cgroup");
}
// Check that we can only do the "allowed_accesses".
foreach(const OpenArgs& args, allowed_accesses) {
if (os::open(args.first, args.second).isError()) {
SAFE_EXIT(EXIT_FAILURE, "Expected allowed read to succeed");
}
}
foreach(const OpenArgs& args, blocked_accesses) {
if (os::open(args.first, args.second).isSome()) {
SAFE_EXIT(EXIT_FAILURE, "Expected blocked read to fail");
}
}
ASSERT_SOME(ebpf::cgroups2::detach(path, attached->at(0)));
// Check that we can do both the "allowed_accesses" and "blocked_accesses".
foreach(const OpenArgs& args, allowed_accesses) {
if (os::open(args.first, args.second).isError()) {
SAFE_EXIT(EXIT_FAILURE, "Expected successful read after detaching"
" device controller program");
}
}
foreach(const OpenArgs& args, blocked_accesses) {
if (os::open(args.first, args.second).isError()) {
SAFE_EXIT(EXIT_FAILURE, "Expected successful read after detaching"
" device controller program");
}
}
::_exit(EXIT_SUCCESS);
}
AWAIT_EXPECT_WEXITSTATUS_EQ(EXIT_SUCCESS, process::reap(pid));
}
INSTANTIATE_TEST_CASE_P(
DeviceControllerTestParams,
DeviceControllerTestFixture,
::testing::Values<DeviceControllerTestParams>(
// Acceses are blocked by default if no entries are configured.
DeviceControllerTestParams{
vector<devices::Entry>{},
vector<devices::Entry>{},
vector<OpenArgs>{},
vector<OpenArgs>{{os::DEV_NULL, O_RDWR}, {os::DEV_NULL, O_RDWR}}
},
// Deny access to /dev/null.
DeviceControllerTestParams{
vector<devices::Entry>{},
vector<devices::Entry>{*devices::Entry::parse("c 1:3 rwm")},
vector<OpenArgs>{},
vector<OpenArgs>{{os::DEV_NULL, O_RDWR}}
},
// Allow access to /dev/null.
DeviceControllerTestParams{
vector<devices::Entry>{*devices::Entry::parse("c 1:3 rwm")},
vector<devices::Entry>{},
vector<OpenArgs>{{os::DEV_NULL, O_RDWR}},
vector<OpenArgs>{}
},
// Allow read-only access to /dev/null.
DeviceControllerTestParams{
vector<devices::Entry>{*devices::Entry::parse("c 1:3 r")},
vector<devices::Entry>{},
vector<OpenArgs>{{os::DEV_NULL, O_RDONLY}},
vector<OpenArgs>{{os::DEV_NULL, O_RDWR}}
},
// Allow write-only access to /dev/null.
DeviceControllerTestParams{
vector<devices::Entry>{*devices::Entry::parse("c 1:3 w")},
vector<devices::Entry>{},
// write-only allowed
vector<OpenArgs>{{os::DEV_NULL, O_WRONLY}},
// read is blocked
vector<OpenArgs>{{os::DEV_NULL, O_RDWR}, {os::DEV_NULL, O_RDONLY}}
},
// Access to /dev/null is denied because the allowed access is to
// a different device type with the same major:minor numbers.
DeviceControllerTestParams{
vector<devices::Entry>{*devices::Entry::parse("b 1:3 rwm")},
vector<devices::Entry>{},
vector<OpenArgs>{},
// /dev/null is blocked
vector<OpenArgs>{{os::DEV_NULL, O_RDWR}, {os::DEV_NULL, O_RDONLY}}
},
// Allow access to all devices.
DeviceControllerTestParams{
vector<devices::Entry>{*devices::Entry::parse("a *:* rwm")},
vector<devices::Entry>{},
vector<OpenArgs>{
{os::DEV_NULL, O_WRONLY},
{os::DEV_NULL, O_RDWR},
{os::DEV_NULL, O_RDONLY}},
vector<OpenArgs>{},
}
));
} // namespace tests {
} // namespace internal {
} // namespace mesos {