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 CgroupController & controller,const std::string & file_name,const std::string & file_v2_name)105 void Reset(const CgroupController& controller, const std::string& file_name,
106 const std::string& file_v2_name) override {
107 CHECK(false);
108 }
controller() const109 const CgroupController* controller() const override {
110 CHECK(false);
111 return {};
112 }
file_name() const113 const std::string& file_name() const override { return file_name_; }
GetPathForProcess(uid_t uid,pid_t pid,std::string * path) const114 bool GetPathForProcess(uid_t uid, pid_t pid, std::string* path) const override {
115 return GetPathForTask(pid, path);
116 }
GetPathForTask(int tid,std::string * path) const117 bool GetPathForTask(int tid, std::string* path) const override {
118 #ifdef __ANDROID__
119 CHECK(CgroupGetControllerPath(CGROUPV2_CONTROLLER_NAME, path));
120 CHECK_GT(path->length(), 0);
121 if (path->rbegin()[0] != '/') {
122 *path += "/";
123 }
124 #else
125 // Not Android.
126 *path = "/sys/fs/cgroup/";
127 #endif
128 *path += file_name_;
129 return true;
130 };
131
GetPathForUID(uid_t,std::string *) const132 bool GetPathForUID(uid_t, std::string*) const override { return false; }
133
134 private:
135 const std::string file_name_;
136 };
137
138 struct TestParam {
139 const char* attr_name;
140 const char* attr_value;
141 bool optional_attr;
142 bool result;
143 LogSeverity log_severity;
144 const char* log_prefix;
145 const char* log_suffix;
146 };
147
148 class SetAttributeFixture : public TestWithParam<TestParam> {
149 public:
150 ~SetAttributeFixture() = default;
151 };
152
TEST_P(SetAttributeFixture,SetAttribute)153 TEST_P(SetAttributeFixture, SetAttribute) {
154 // Treehugger runs host tests inside a container either without cgroupv2
155 // support or with the cgroup filesystem mounted read-only.
156 if (!IsCgroupV2MountedRw()) {
157 GTEST_SKIP();
158 return;
159 }
160 const TestParam params = GetParam();
161 ScopedLogCapturer captured_log;
162 ProfileAttributeMock pa(params.attr_name);
163 SetAttributeAction a(&pa, params.attr_value, params.optional_attr);
164 EXPECT_EQ(a.ExecuteForProcess(getuid(), getpid()), params.result);
165 auto log = captured_log.Log();
166 if (params.log_prefix || params.log_suffix) {
167 ASSERT_EQ(log.size(), 1);
168 EXPECT_EQ(log[0].severity, params.log_severity);
169 if (params.log_prefix) {
170 EXPECT_EQ(log[0].message.find(params.log_prefix), 0);
171 }
172 if (params.log_suffix) {
173 EXPECT_NE(log[0].message.find(params.log_suffix), std::string::npos);
174 }
175 } else {
176 ASSERT_EQ(log.size(), 0);
177 }
178 }
179
180 class TaskProfileFixture : public TestWithParam<TestParam> {
181 public:
182 ~TaskProfileFixture() = default;
183 };
184
TEST_P(TaskProfileFixture,TaskProfile)185 TEST_P(TaskProfileFixture, TaskProfile) {
186 // Treehugger runs host tests inside a container without cgroupv2 support.
187 if (!IsCgroupV2MountedRw()) {
188 GTEST_SKIP();
189 return;
190 }
191 const TestParam params = GetParam();
192 ProfileAttributeMock pa(params.attr_name);
193 // Test simple profile with one action
194 std::shared_ptr<TaskProfile> tp = std::make_shared<TaskProfile>("test_profile");
195 tp->Add(std::make_unique<SetAttributeAction>(&pa, params.attr_value, params.optional_attr));
196 EXPECT_EQ(tp->IsValidForProcess(getuid(), getpid()), params.result);
197 EXPECT_EQ(tp->IsValidForTask(getpid()), params.result);
198 // Test aggregate profile
199 TaskProfile tp2("meta_profile");
200 std::vector<std::shared_ptr<TaskProfile>> profiles = {tp};
201 tp2.Add(std::make_unique<ApplyProfileAction>(profiles));
202 EXPECT_EQ(tp2.IsValidForProcess(getuid(), getpid()), params.result);
203 EXPECT_EQ(tp2.IsValidForTask(getpid()), params.result);
204 }
205
206 // Test the four combinations of optional_attr {false, true} and cgroup attribute { does not exist,
207 // exists }.
208 INSTANTIATE_TEST_SUITE_P(
209 SetAttributeTestSuite, SetAttributeFixture,
210 Values(
211 // Test that attempting to write into a non-existing cgroup attribute fails and also
212 // that an error message is logged.
213 TestParam{.attr_name = "no-such-attribute",
214 .attr_value = ".",
215 .optional_attr = false,
216 .result = false,
217 .log_severity = ERROR,
218 .log_prefix = "No such cgroup attribute"},
219 // Test that attempting to write into an optional non-existing cgroup attribute
220 // results in the return value 'true' and also that no messages are logged.
221 TestParam{.attr_name = "no-such-attribute",
222 .attr_value = ".",
223 .optional_attr = true,
224 .result = true},
225 // Test that attempting to write an invalid value into an existing optional cgroup
226 // attribute fails and also that it causes an error
227 // message to be logged.
228 TestParam{.attr_name = "cgroup.procs",
229 .attr_value = "-1",
230 .optional_attr = true,
231 .result = false,
232 .log_severity = ERROR,
233 .log_prefix = "Failed to write",
234 .log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"},
235 // Test that attempting to write into an existing optional read-only cgroup
236 // attribute fails and also that it causes an error message to be logged.
237 TestParam{
238 .attr_name = "cgroup.controllers",
239 .attr_value = ".",
240 .optional_attr = false,
241 .result = false,
242 .log_severity = ERROR,
243 .log_prefix = "Failed to write",
244 .log_suffix = geteuid() == 0 ? "Invalid argument" : "Permission denied"}));
245
246 // Test TaskProfile IsValid calls.
247 INSTANTIATE_TEST_SUITE_P(
248 TaskProfileTestSuite, TaskProfileFixture,
249 Values(
250 // Test operating on non-existing cgroup attribute fails.
251 TestParam{.attr_name = "no-such-attribute",
252 .attr_value = ".",
253 .optional_attr = false,
254 .result = false},
255 // Test operating on optional non-existing cgroup attribute succeeds.
256 TestParam{.attr_name = "no-such-attribute",
257 .attr_value = ".",
258 .optional_attr = true,
259 .result = true},
260 // Test operating on existing cgroup attribute succeeds.
261 TestParam{.attr_name = "cgroup.procs",
262 .attr_value = ".",
263 .optional_attr = false,
264 .result = true},
265 // Test operating on optional existing cgroup attribute succeeds.
266 TestParam{.attr_name = "cgroup.procs",
267 .attr_value = ".",
268 .optional_attr = true,
269 .result = true}));
270 } // namespace
271