blob: b17e695e562b5d8eb3f7851ed4be5d5335cbdbbc [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed 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 "task_profiles.h"
#include <android-base/logging.h>
#include <android-base/strings.h>
#include <gtest/gtest.h>
#include <mntent.h>
#include <processgroup/processgroup.h>
#include <stdio.h>
#include <unistd.h>
#include <fstream>
using ::android::base::ERROR;
using ::android::base::LogFunction;
using ::android::base::LogId;
using ::android::base::LogSeverity;
using ::android::base::SetLogger;
using ::android::base::Split;
using ::android::base::VERBOSE;
using ::testing::TestWithParam;
using ::testing::Values;
namespace {
bool IsCgroupV2MountedRw() {
std::unique_ptr<FILE, int (*)(FILE*)> mnts(setmntent("/proc/mounts", "re"), endmntent);
if (!mnts) {
LOG(ERROR) << "Failed to open /proc/mounts";
return false;
}
struct mntent* mnt;
while ((mnt = getmntent(mnts.get()))) {
if (strcmp(mnt->mnt_type, "cgroup2") != 0) {
continue;
}
const std::vector<std::string> options = Split(mnt->mnt_opts, ",");
return std::count(options.begin(), options.end(), "ro") == 0;
}
return false;
}
class ScopedLogCapturer {
public:
struct log_args {
LogId log_buffer_id;
LogSeverity severity;
std::string tag;
std::string file;
unsigned int line;
std::string message;
};
// Constructor. Installs a new logger and saves the currently active logger.
ScopedLogCapturer() {
saved_severity_ = SetMinimumLogSeverity(android::base::VERBOSE);
saved_logger_ = SetLogger([this](LogId log_buffer_id, LogSeverity severity, const char* tag,
const char* file, unsigned int line, const char* message) {
if (saved_logger_) {
saved_logger_(log_buffer_id, severity, tag, file, line, message);
}
log_.emplace_back(log_args{.log_buffer_id = log_buffer_id,
.severity = severity,
.tag = tag,
.file = file,
.line = line,
.message = message});
});
}
// Destructor. Restores the original logger and log level.
~ScopedLogCapturer() {
SetLogger(std::move(saved_logger_));
SetMinimumLogSeverity(saved_severity_);
}
ScopedLogCapturer(const ScopedLogCapturer&) = delete;
ScopedLogCapturer& operator=(const ScopedLogCapturer&) = delete;
// Returns the logged lines.
const std::vector<log_args>& Log() const { return log_; }
private:
LogSeverity saved_severity_;
LogFunction saved_logger_;
std::vector<log_args> log_;
};
// cgroup attribute at the top level of the cgroup hierarchy.
class ProfileAttributeMock : public IProfileAttribute {
public:
ProfileAttributeMock(const std::string& file_name) : file_name_(file_name) {}
~ProfileAttributeMock() override = default;
void Reset(const CgroupController& controller, const std::string& file_name,
const std::string& file_v2_name) override {
CHECK(false);
}
const CgroupController* controller() const override {
CHECK(false);
return {};
}
const std::string& file_name() const override { return file_name_; }
bool GetPathForProcess(uid_t uid, pid_t pid, std::string* path) const override {
return GetPathForTask(pid, path);
}
bool GetPathForTask(int tid, std::string* path) const override {
#ifdef __ANDROID__
CHECK(CgroupGetControllerPath(CGROUPV2_HIERARCHY_NAME, path));
CHECK_GT(path->length(), 0);
if (path->rbegin()[0] != '/') {
*path += "/";
}
#else
// Not Android.
*path = "/sys/fs/cgroup/";
#endif
*path += file_name_;
return true;
};
bool GetPathForUID(uid_t, std::string*) const override { return false; }
private:
const std::string file_name_;
};
struct TestParam {
const char* attr_name;
const char* attr_value;
bool optional_attr;
bool result;
LogSeverity log_severity;
const char* log_prefix;
const char* log_suffix;
};
class SetAttributeFixture : public TestWithParam<TestParam> {
public:
~SetAttributeFixture() = default;
};
TEST_P(SetAttributeFixture, SetAttribute) {
// Treehugger runs host tests inside a container either without cgroupv2
// support or with the cgroup filesystem mounted read-only.
if (!IsCgroupV2MountedRw()) {
GTEST_SKIP();
return;
}
const TestParam params = GetParam();
ScopedLogCapturer captured_log;
ProfileAttributeMock pa(params.attr_name);
SetAttributeAction a(&pa, params.attr_value, params.optional_attr);
EXPECT_EQ(a.ExecuteForProcess(getuid(), getpid()), params.result);
auto log = captured_log.Log();
if (params.log_prefix || params.log_suffix) {
ASSERT_EQ(log.size(), 1);
EXPECT_EQ(log[0].severity, params.log_severity);
if (params.log_prefix) {
EXPECT_EQ(log[0].message.find(params.log_prefix), 0);
}
if (params.log_suffix) {
EXPECT_NE(log[0].message.find(params.log_suffix), std::string::npos);
}
} else {
ASSERT_EQ(log.size(), 0);
}
}
class TaskProfileFixture : public TestWithParam<TestParam> {
public:
~TaskProfileFixture() = default;
};
TEST_P(TaskProfileFixture, TaskProfile) {
// Treehugger runs host tests inside a container without cgroupv2 support.
if (!IsCgroupV2MountedRw()) {
GTEST_SKIP();
return;
}
const TestParam params = GetParam();
ProfileAttributeMock pa(params.attr_name);
// Test simple profile with one action
std::shared_ptr<TaskProfile> tp = std::make_shared<TaskProfile>("test_profile");
tp->Add(std::make_unique<SetAttributeAction>(&pa, params.attr_value, params.optional_attr));
EXPECT_EQ(tp->IsValidForProcess(getuid(), getpid()), params.result);
EXPECT_EQ(tp->IsValidForTask(getpid()), params.result);
// Test aggregate profile
TaskProfile tp2("meta_profile");
std::vector<std::shared_ptr<TaskProfile>> profiles = {tp};
tp2.Add(std::make_unique<ApplyProfileAction>(profiles));
EXPECT_EQ(tp2.IsValidForProcess(getuid(), getpid()), params.result);
EXPECT_EQ(tp2.IsValidForTask(getpid()), params.result);
}
// Test the four combinations of optional_attr {false, true} and cgroup attribute { does not exist,
// exists }.
INSTANTIATE_TEST_SUITE_P(
SetAttributeTestSuite, SetAttributeFixture,
Values(
// Test that attempting to write into a non-existing cgroup attribute fails and also
// that an error message is logged.
TestParam{.attr_name = "no-such-attribute",
.attr_value = ".",
.optional_attr = false,
.result = false,
.log_severity = ERROR,
.log_prefix = "No such cgroup attribute"},
// Test that attempting to write into an optional non-existing cgroup attribute
// results in the return value 'true' and also that no messages are logged.
TestParam{.attr_name = "no-such-attribute",
.attr_value = ".",
.optional_attr = true,
.result = true},
// Test that attempting to write an invalid value into an existing optional cgroup
// attribute fails and also that it causes an error
// message to be logged.
TestParam{.attr_name = "cgroup.procs",
.attr_value = "-1",
.optional_attr = true,
.result = false,
.log_severity = ERROR,
.log_prefix = "Failed to write",
.log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"},
// Test that attempting to write into an existing optional read-only cgroup
// attribute fails and also that it causes an error message to be logged.
TestParam{
.attr_name = "cgroup.controllers",
.attr_value = ".",
.optional_attr = false,
.result = false,
.log_severity = ERROR,
.log_prefix = "Failed to write",
.log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"}));
// Test TaskProfile IsValid calls.
INSTANTIATE_TEST_SUITE_P(
TaskProfileTestSuite, TaskProfileFixture,
Values(
// Test operating on non-existing cgroup attribute fails.
TestParam{.attr_name = "no-such-attribute",
.attr_value = ".",
.optional_attr = false,
.result = false},
// Test operating on optional non-existing cgroup attribute succeeds.
TestParam{.attr_name = "no-such-attribute",
.attr_value = ".",
.optional_attr = true,
.result = true},
// Test operating on existing cgroup attribute succeeds.
TestParam{.attr_name = "cgroup.procs",
.attr_value = ".",
.optional_attr = false,
.result = true},
// Test operating on optional existing cgroup attribute succeeds.
TestParam{.attr_name = "cgroup.procs",
.attr_value = ".",
.optional_attr = true,
.result = true}));
} // namespace