1 //
2 // Copyright 2022 The Abseil Authors.
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 // https://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 // Tests for stripping of literal strings.
17 // ---------------------------------------
18 //
19 // When a `LOG` statement can be trivially proved at compile time to never fire,
20 // e.g. due to `ABSL_MIN_LOG_LEVEL`, `NDEBUG`, or some explicit condition, data
21 // streamed in can be dropped from the compiled program completely if they are
22 // not used elsewhere. This most commonly affects string literals, which users
23 // often want to strip to reduce binary size and/or redact information about
24 // their program's internals (e.g. in a release build).
25 //
26 // These tests log strings and then validate whether they appear in the compiled
27 // binary. This is done by opening the file corresponding to the running test
28 // and running a simple string search on its contents. The strings to be logged
29 // and searched for must be unique, and we must take care not to emit them into
30 // the binary in any other place, e.g. when searching for them. The latter is
31 // accomplished by computing them using base64; the source string appears in the
32 // binary but the target string is computed at runtime.
33
34 #include <stdio.h>
35
36 #if defined(__MACH__)
37 #include <mach-o/dyld.h>
38 #elif defined(_WIN32)
39 #include <Windows.h>
40 #include <tchar.h>
41 #endif
42
43 #include <algorithm>
44 #include <functional>
45 #include <memory>
46 #include <ostream>
47 #include <string>
48
49 #include "gmock/gmock.h"
50 #include "gtest/gtest.h"
51 #include "absl/base/internal/strerror.h"
52 #include "absl/flags/internal/program_name.h"
53 #include "absl/log/check.h"
54 #include "absl/log/internal/test_helpers.h"
55 #include "absl/log/log.h"
56 #include "absl/strings/escaping.h"
57 #include "absl/strings/str_format.h"
58 #include "absl/strings/string_view.h"
59
60 namespace {
61 using ::testing::_;
62 using ::testing::Eq;
63 using ::testing::NotNull;
64
65 using absl::log_internal::kAbslMinLogLevel;
66
Base64UnescapeOrDie(absl::string_view data)67 std::string Base64UnescapeOrDie(absl::string_view data) {
68 std::string decoded;
69 CHECK(absl::Base64Unescape(data, &decoded));
70 return decoded;
71 }
72
73 // -----------------------------------------------------------------------------
74 // A Googletest matcher which searches the running binary for a given string
75 // -----------------------------------------------------------------------------
76
77 // This matcher is used to validate that literal strings streamed into
78 // `LOG` statements that ought to be compiled out (e.g. `LOG_IF(INFO, false)`)
79 // do not appear in the binary.
80 //
81 // Note that passing the string to be sought directly to `FileHasSubstr()` all
82 // but forces its inclusion in the binary regardless of the logging library's
83 // behavior. For example:
84 //
85 // LOG_IF(INFO, false) << "you're the man now dog";
86 // // This will always pass:
87 // // EXPECT_THAT(fp, FileHasSubstr("you're the man now dog"));
88 // // So use this instead:
89 // EXPECT_THAT(fp, FileHasSubstr(
90 // Base64UnescapeOrDie("eW91J3JlIHRoZSBtYW4gbm93IGRvZw==")));
91
92 class FileHasSubstrMatcher final : public ::testing::MatcherInterface<FILE*> {
93 public:
FileHasSubstrMatcher(absl::string_view needle)94 explicit FileHasSubstrMatcher(absl::string_view needle) : needle_(needle) {}
95
MatchAndExplain(FILE * fp,::testing::MatchResultListener * listener) const96 bool MatchAndExplain(
97 FILE* fp, ::testing::MatchResultListener* listener) const override {
98 std::string buf(
99 std::max<std::string::size_type>(needle_.size() * 2, 163840000), '\0');
100 size_t buf_start_offset = 0; // The file offset of the byte at `buf[0]`.
101 size_t buf_data_size = 0; // The number of bytes of `buf` which contain
102 // data.
103
104 ::fseek(fp, 0, SEEK_SET);
105 while (true) {
106 // Fill the buffer to capacity or EOF:
107 while (buf_data_size < buf.size()) {
108 const size_t ret = fread(&buf[buf_data_size], sizeof(char),
109 buf.size() - buf_data_size, fp);
110 if (ret == 0) break;
111 buf_data_size += ret;
112 }
113 if (ferror(fp)) {
114 *listener << "error reading file";
115 return false;
116 }
117 const absl::string_view haystack(&buf[0], buf_data_size);
118 const auto off = haystack.find(needle_);
119 if (off != haystack.npos) {
120 *listener << "string found at offset " << buf_start_offset + off;
121 return true;
122 }
123 if (feof(fp)) {
124 *listener << "string not found";
125 return false;
126 }
127 // Copy the end of `buf` to the beginning so we catch matches that span
128 // buffer boundaries. `buf` and `buf_data_size` are always large enough
129 // that these ranges don't overlap.
130 memcpy(&buf[0], &buf[buf_data_size - needle_.size()], needle_.size());
131 buf_start_offset += buf_data_size - needle_.size();
132 buf_data_size = needle_.size();
133 }
134 }
DescribeTo(std::ostream * os) const135 void DescribeTo(std::ostream* os) const override {
136 *os << "contains the string \"" << needle_ << "\" (base64(\""
137 << Base64UnescapeOrDie(needle_) << "\"))";
138 }
139
DescribeNegationTo(std::ostream * os) const140 void DescribeNegationTo(std::ostream* os) const override {
141 *os << "does not ";
142 DescribeTo(os);
143 }
144
145 private:
146 std::string needle_;
147 };
148
149 class StrippingTest : public ::testing::Test {
150 protected:
SetUp()151 void SetUp() override {
152 #ifndef NDEBUG
153 // Non-optimized builds don't necessarily eliminate dead code at all, so we
154 // don't attempt to validate stripping against such builds.
155 GTEST_SKIP() << "StrippingTests skipped since this build is not optimized";
156 #elif defined(__EMSCRIPTEN__)
157 // These tests require a way to examine the running binary and look for
158 // strings; there's no portable way to do that.
159 GTEST_SKIP()
160 << "StrippingTests skipped since this platform is not optimized";
161 #endif
162 }
163
164 // Opens this program's executable file. Returns `nullptr` and writes to
165 // `stderr` on failure.
OpenTestExecutable()166 std::unique_ptr<FILE, std::function<void(FILE*)>> OpenTestExecutable() {
167 #if defined(__linux__)
168 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
169 fopen("/proc/self/exe", "rb"), [](FILE* fp) { fclose(fp); });
170 if (!fp) {
171 const std::string err = absl::base_internal::StrError(errno);
172 absl::FPrintF(stderr, "Failed to open /proc/self/exe: %s\n", err);
173 }
174 return fp;
175 #elif defined(__Fuchsia__)
176 // TODO(b/242579714): We need to restore the test coverage on this platform.
177 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
178 fopen(absl::StrCat("/pkg/bin/",
179 absl::flags_internal::ShortProgramInvocationName())
180 .c_str(),
181 "rb"),
182 [](FILE* fp) { fclose(fp); });
183 if (!fp) {
184 const std::string err = absl::base_internal::StrError(errno);
185 absl::FPrintF(stderr, "Failed to open /pkg/bin/<binary name>: %s\n", err);
186 }
187 return fp;
188 #elif defined(__MACH__)
189 uint32_t size = 0;
190 int ret = _NSGetExecutablePath(nullptr, &size);
191 if (ret != -1) {
192 absl::FPrintF(stderr,
193 "Failed to get executable path: "
194 "_NSGetExecutablePath(nullptr) returned %d\n",
195 ret);
196 return nullptr;
197 }
198 std::string path(size, '\0');
199 ret = _NSGetExecutablePath(&path[0], &size);
200 if (ret != 0) {
201 absl::FPrintF(
202 stderr,
203 "Failed to get executable path: _NSGetExecutablePath(buffer) "
204 "returned %d\n",
205 ret);
206 return nullptr;
207 }
208 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
209 fopen(path.c_str(), "rb"), [](FILE* fp) { fclose(fp); });
210 if (!fp) {
211 const std::string err = absl::base_internal::StrError(errno);
212 absl::FPrintF(stderr, "Failed to open executable at %s: %s\n", path, err);
213 }
214 return fp;
215 #elif defined(_WIN32)
216 std::basic_string<TCHAR> path(4096, _T('\0'));
217 while (true) {
218 const uint32_t ret = ::GetModuleFileName(nullptr, &path[0],
219 static_cast<DWORD>(path.size()));
220 if (ret == 0) {
221 absl::FPrintF(
222 stderr,
223 "Failed to get executable path: GetModuleFileName(buffer) "
224 "returned 0\n");
225 return nullptr;
226 }
227 if (ret < path.size()) break;
228 path.resize(path.size() * 2, _T('\0'));
229 }
230 std::unique_ptr<FILE, std::function<void(FILE*)>> fp(
231 _tfopen(path.c_str(), _T("rb")), [](FILE* fp) { fclose(fp); });
232 if (!fp) absl::FPrintF(stderr, "Failed to open executable\n");
233 return fp;
234 #else
235 absl::FPrintF(stderr,
236 "OpenTestExecutable() unimplemented on this platform\n");
237 return nullptr;
238 #endif
239 }
240
FileHasSubstr(absl::string_view needle)241 ::testing::Matcher<FILE*> FileHasSubstr(absl::string_view needle) {
242 return MakeMatcher(new FileHasSubstrMatcher(needle));
243 }
244 };
245
246 // This tests whether out methodology for testing stripping works on this
247 // platform by looking for one string that definitely ought to be there and one
248 // that definitely ought not to. If this fails, none of the `StrippingTest`s
249 // are going to produce meaningful results.
TEST_F(StrippingTest,Control)250 TEST_F(StrippingTest, Control) {
251 constexpr char kEncodedPositiveControl[] =
252 "U3RyaXBwaW5nVGVzdC5Qb3NpdGl2ZUNvbnRyb2w=";
253 const std::string encoded_negative_control =
254 absl::Base64Escape("StrippingTest.NegativeControl");
255
256 // Verify this mainly so we can encode other strings and know definitely they
257 // won't encode to `kEncodedPositiveControl`.
258 EXPECT_THAT(Base64UnescapeOrDie("U3RyaXBwaW5nVGVzdC5Qb3NpdGl2ZUNvbnRyb2w="),
259 Eq("StrippingTest.PositiveControl"));
260
261 auto exe = OpenTestExecutable();
262 ASSERT_THAT(exe, NotNull());
263 EXPECT_THAT(exe.get(), FileHasSubstr(kEncodedPositiveControl));
264 EXPECT_THAT(exe.get(), Not(FileHasSubstr(encoded_negative_control)));
265 }
266
TEST_F(StrippingTest,Literal)267 TEST_F(StrippingTest, Literal) {
268 // We need to load a copy of the needle string into memory (so we can search
269 // for it) without leaving it lying around in plaintext in the executable file
270 // as would happen if we used a literal. We might (or might not) leave it
271 // lying around later; that's what the tests are for!
272 const std::string needle = absl::Base64Escape("StrippingTest.Literal");
273 LOG(INFO) << "U3RyaXBwaW5nVGVzdC5MaXRlcmFs";
274 auto exe = OpenTestExecutable();
275 ASSERT_THAT(exe, NotNull());
276 if (absl::LogSeverity::kInfo >= kAbslMinLogLevel) {
277 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
278 } else {
279 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
280 }
281 }
282
TEST_F(StrippingTest,LiteralInExpression)283 TEST_F(StrippingTest, LiteralInExpression) {
284 // We need to load a copy of the needle string into memory (so we can search
285 // for it) without leaving it lying around in plaintext in the executable file
286 // as would happen if we used a literal. We might (or might not) leave it
287 // lying around later; that's what the tests are for!
288 const std::string needle =
289 absl::Base64Escape("StrippingTest.LiteralInExpression");
290 LOG(INFO) << absl::StrCat("secret: ",
291 "U3RyaXBwaW5nVGVzdC5MaXRlcmFsSW5FeHByZXNzaW9u");
292 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
293 ASSERT_THAT(exe, NotNull());
294 if (absl::LogSeverity::kInfo >= kAbslMinLogLevel) {
295 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
296 } else {
297 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
298 }
299 }
300
TEST_F(StrippingTest,Fatal)301 TEST_F(StrippingTest, Fatal) {
302 // We need to load a copy of the needle string into memory (so we can search
303 // for it) without leaving it lying around in plaintext in the executable file
304 // as would happen if we used a literal. We might (or might not) leave it
305 // lying around later; that's what the tests are for!
306 const std::string needle = absl::Base64Escape("StrippingTest.Fatal");
307 EXPECT_DEATH_IF_SUPPORTED(LOG(FATAL) << "U3RyaXBwaW5nVGVzdC5GYXRhbA==", "");
308 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
309 ASSERT_THAT(exe, NotNull());
310 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
311 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
312 } else {
313 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
314 }
315 }
316
TEST_F(StrippingTest,Level)317 TEST_F(StrippingTest, Level) {
318 const std::string needle = absl::Base64Escape("StrippingTest.Level");
319 volatile auto severity = absl::LogSeverity::kWarning;
320 // Ensure that `severity` is not a compile-time constant to prove that
321 // stripping works regardless:
322 LOG(LEVEL(severity)) << "U3RyaXBwaW5nVGVzdC5MZXZlbA==";
323 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable();
324 ASSERT_THAT(exe, NotNull());
325 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) {
326 // This can't be stripped at compile-time because it might evaluate to a
327 // level that shouldn't be stripped.
328 EXPECT_THAT(exe.get(), FileHasSubstr(needle));
329 } else {
330 #if defined(_MSC_VER) || defined(__APPLE__)
331 // Dead code elimination misses this case.
332 #else
333 // All levels should be stripped, so it doesn't matter what the severity
334 // winds up being.
335 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle)));
336 #endif
337 }
338 }
339
340 } // namespace
341