1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #include "task_profiles.h" 18 #include <android-base/logging.h> 19 #include <android-base/strings.h> 20 #include <gtest/gtest.h> 21 #include <mntent.h> 22 #include <processgroup/processgroup.h> 23 #include <stdio.h> 24 #include <unistd.h> 25 26 #include <fstream> 27 28 using ::android::base::ERROR; 29 using ::android::base::LogFunction; 30 using ::android::base::LogId; 31 using ::android::base::LogSeverity; 32 using ::android::base::SetLogger; 33 using ::android::base::Split; 34 using ::android::base::VERBOSE; 35 using ::testing::TestWithParam; 36 using ::testing::Values; 37 38 namespace { 39 IsCgroupV2MountedRw()40 bool IsCgroupV2MountedRw() { 41 std::unique_ptr<FILE, int (*)(FILE*)> mnts(setmntent("/proc/mounts", "re"), endmntent); 42 if (!mnts) { 43 LOG(ERROR) << "Failed to open /proc/mounts"; 44 return false; 45 } 46 struct mntent* mnt; 47 while ((mnt = getmntent(mnts.get()))) { 48 if (strcmp(mnt->mnt_type, "cgroup2") != 0) { 49 continue; 50 } 51 const std::vector<std::string> options = Split(mnt->mnt_opts, ","); 52 return std::count(options.begin(), options.end(), "ro") == 0; 53 } 54 return false; 55 } 56 57 class ScopedLogCapturer { 58 public: 59 struct log_args { 60 LogId log_buffer_id; 61 LogSeverity severity; 62 std::string tag; 63 std::string file; 64 unsigned int line; 65 std::string message; 66 }; 67 68 // Constructor. Installs a new logger and saves the currently active logger. ScopedLogCapturer()69 ScopedLogCapturer() { 70 saved_severity_ = SetMinimumLogSeverity(android::base::VERBOSE); 71 saved_logger_ = SetLogger([this](LogId log_buffer_id, LogSeverity severity, const char* tag, 72 const char* file, unsigned int line, const char* message) { 73 if (saved_logger_) { 74 saved_logger_(log_buffer_id, severity, tag, file, line, message); 75 } 76 log_.emplace_back(log_args{.log_buffer_id = log_buffer_id, 77 .severity = severity, 78 .tag = tag, 79 .file = file, 80 .line = line, 81 .message = message}); 82 }); 83 } 84 // Destructor. Restores the original logger and log level. ~ScopedLogCapturer()85 ~ScopedLogCapturer() { 86 SetLogger(std::move(saved_logger_)); 87 SetMinimumLogSeverity(saved_severity_); 88 } 89 ScopedLogCapturer(const ScopedLogCapturer&) = delete; 90 ScopedLogCapturer& operator=(const ScopedLogCapturer&) = delete; 91 // Returns the logged lines. Log() const92 const std::vector<log_args>& Log() const { return log_; } 93 94 private: 95 LogSeverity saved_severity_; 96 LogFunction saved_logger_; 97 std::vector<log_args> log_; 98 }; 99 100 // cgroup attribute at the top level of the cgroup hierarchy. 101 class ProfileAttributeMock : public IProfileAttribute { 102 public: ProfileAttributeMock(const std::string & file_name)103 ProfileAttributeMock(const std::string& file_name) : file_name_(file_name) {} 104 ~ProfileAttributeMock() override = default; Reset(const CgroupControllerWrapper &,const std::string &,const std::string &)105 void Reset(const CgroupControllerWrapper&, const std::string&, const std::string&) override { 106 CHECK(false); 107 } controller() const108 const CgroupControllerWrapper* controller() const override { 109 CHECK(false); 110 return {}; 111 } file_name() const112 const std::string& file_name() const override { return file_name_; } GetPathForProcess(uid_t,pid_t pid,std::string * path) const113 bool GetPathForProcess(uid_t, pid_t pid, std::string* path) const override { 114 return GetPathForTask(pid, path); 115 } GetPathForTask(int,std::string * path) const116 bool GetPathForTask(int, std::string* path) const override { 117 #ifdef __ANDROID__ 118 CHECK(CgroupGetControllerPath(CGROUPV2_HIERARCHY_NAME, path)); 119 CHECK_GT(path->length(), 0); 120 if (path->rbegin()[0] != '/') { 121 *path += "/"; 122 } 123 #else 124 // Not Android. 125 *path = "/sys/fs/cgroup/"; 126 #endif 127 *path += file_name_; 128 return true; 129 }; 130 GetPathForUID(uid_t,std::string *) const131 bool GetPathForUID(uid_t, std::string*) const override { return false; } 132 133 private: 134 const std::string file_name_; 135 }; 136 137 struct TestParam { 138 const char* attr_name; 139 const char* attr_value; 140 bool optional_attr; 141 bool result; 142 LogSeverity log_severity; 143 const char* log_prefix; 144 const char* log_suffix; 145 }; 146 147 class SetAttributeFixture : public TestWithParam<TestParam> { 148 public: 149 ~SetAttributeFixture() = default; 150 }; 151 TEST_P(SetAttributeFixture,SetAttribute)152 TEST_P(SetAttributeFixture, SetAttribute) { 153 // Treehugger runs host tests inside a container either without cgroupv2 154 // support or with the cgroup filesystem mounted read-only. 155 if (!IsCgroupV2MountedRw()) { 156 GTEST_SKIP(); 157 return; 158 } 159 const TestParam params = GetParam(); 160 ScopedLogCapturer captured_log; 161 ProfileAttributeMock pa(params.attr_name); 162 SetAttributeAction a(&pa, params.attr_value, params.optional_attr); 163 EXPECT_EQ(a.ExecuteForProcess(getuid(), getpid()), params.result); 164 auto log = captured_log.Log(); 165 if (params.log_prefix || params.log_suffix) { 166 ASSERT_EQ(log.size(), 1); 167 EXPECT_EQ(log[0].severity, params.log_severity); 168 if (params.log_prefix) { 169 EXPECT_EQ(log[0].message.find(params.log_prefix), 0); 170 } 171 if (params.log_suffix) { 172 EXPECT_NE(log[0].message.find(params.log_suffix), std::string::npos); 173 } 174 } else { 175 ASSERT_EQ(log.size(), 0); 176 } 177 } 178 179 class TaskProfileFixture : public TestWithParam<TestParam> { 180 public: 181 ~TaskProfileFixture() = default; 182 }; 183 TEST_P(TaskProfileFixture,TaskProfile)184 TEST_P(TaskProfileFixture, TaskProfile) { 185 // Treehugger runs host tests inside a container without cgroupv2 support. 186 if (!IsCgroupV2MountedRw()) { 187 GTEST_SKIP(); 188 return; 189 } 190 const TestParam params = GetParam(); 191 ProfileAttributeMock pa(params.attr_name); 192 // Test simple profile with one action 193 std::shared_ptr<TaskProfile> tp = std::make_shared<TaskProfile>("test_profile"); 194 tp->Add(std::make_unique<SetAttributeAction>(&pa, params.attr_value, params.optional_attr)); 195 EXPECT_EQ(tp->IsValidForProcess(getuid(), getpid()), params.result); 196 EXPECT_EQ(tp->IsValidForTask(getpid()), params.result); 197 // Test aggregate profile 198 TaskProfile tp2("meta_profile"); 199 std::vector<std::shared_ptr<TaskProfile>> profiles = {tp}; 200 tp2.Add(std::make_unique<ApplyProfileAction>(profiles)); 201 EXPECT_EQ(tp2.IsValidForProcess(getuid(), getpid()), params.result); 202 EXPECT_EQ(tp2.IsValidForTask(getpid()), params.result); 203 } 204 205 // Test the four combinations of optional_attr {false, true} and cgroup attribute { does not exist, 206 // exists }. 207 INSTANTIATE_TEST_SUITE_P( 208 SetAttributeTestSuite, SetAttributeFixture, 209 Values( 210 // Test that attempting to write into a non-existing cgroup attribute fails and also 211 // that an error message is logged. 212 TestParam{.attr_name = "no-such-attribute", 213 .attr_value = ".", 214 .optional_attr = false, 215 .result = false, 216 .log_severity = ERROR, 217 .log_prefix = "No such cgroup attribute"}, 218 // Test that attempting to write into an optional non-existing cgroup attribute 219 // results in the return value 'true' and also that no messages are logged. 220 TestParam{.attr_name = "no-such-attribute", 221 .attr_value = ".", 222 .optional_attr = true, 223 .result = true}, 224 // Test that attempting to write an invalid value into an existing optional cgroup 225 // attribute fails and also that it causes an error 226 // message to be logged. 227 TestParam{.attr_name = "cgroup.procs", 228 .attr_value = "-1", 229 .optional_attr = true, 230 .result = false, 231 .log_severity = ERROR, 232 .log_prefix = "Failed to write", 233 .log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"}, 234 // Test that attempting to write into an existing optional read-only cgroup 235 // attribute fails and also that it causes an error message to be logged. 236 TestParam{ 237 .attr_name = "cgroup.controllers", 238 .attr_value = ".", 239 .optional_attr = false, 240 .result = false, 241 .log_severity = ERROR, 242 .log_prefix = "Failed to write", 243 .log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"})); 244 245 // Test TaskProfile IsValid calls. 246 INSTANTIATE_TEST_SUITE_P( 247 TaskProfileTestSuite, TaskProfileFixture, 248 Values( 249 // Test operating on non-existing cgroup attribute fails. 250 TestParam{.attr_name = "no-such-attribute", 251 .attr_value = ".", 252 .optional_attr = false, 253 .result = false}, 254 // Test operating on optional non-existing cgroup attribute succeeds. 255 TestParam{.attr_name = "no-such-attribute", 256 .attr_value = ".", 257 .optional_attr = true, 258 .result = true}, 259 // Test operating on existing cgroup attribute succeeds. 260 TestParam{.attr_name = "cgroup.procs", 261 .attr_value = ".", 262 .optional_attr = false, 263 .result = true}, 264 // Test operating on optional existing cgroup attribute succeeds. 265 TestParam{.attr_name = "cgroup.procs", 266 .attr_value = ".", 267 .optional_attr = true, 268 .result = true})); 269 } // namespace 270