stripping_test.cc (20408B)
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/base/log_severity.h" 53 #include "absl/flags/internal/program_name.h" 54 #include "absl/log/check.h" 55 #include "absl/log/internal/test_helpers.h" 56 #include "absl/log/log.h" 57 #include "absl/status/status.h" 58 #include "absl/strings/escaping.h" 59 #include "absl/strings/str_format.h" 60 #include "absl/strings/string_view.h" 61 62 // Set a flag that controls whether we actually execute fatal statements, but 63 // prevent the compiler from optimizing it out. 64 static volatile bool kReallyDie = false; 65 66 namespace { 67 using ::testing::_; 68 using ::testing::Eq; 69 using ::testing::NotNull; 70 71 using absl::log_internal::kAbslMinLogLevel; 72 73 std::string Base64UnescapeOrDie(absl::string_view data) { 74 std::string decoded; 75 CHECK(absl::Base64Unescape(data, &decoded)); 76 return decoded; 77 } 78 79 // ----------------------------------------------------------------------------- 80 // A Googletest matcher which searches the running binary for a given string 81 // ----------------------------------------------------------------------------- 82 83 // This matcher is used to validate that literal strings streamed into 84 // `LOG` statements that ought to be compiled out (e.g. `LOG_IF(INFO, false)`) 85 // do not appear in the binary. 86 // 87 // Note that passing the string to be sought directly to `FileHasSubstr()` all 88 // but forces its inclusion in the binary regardless of the logging library's 89 // behavior. For example: 90 // 91 // LOG_IF(INFO, false) << "you're the man now dog"; 92 // // This will always pass: 93 // // EXPECT_THAT(fp, FileHasSubstr("you're the man now dog")); 94 // // So use this instead: 95 // EXPECT_THAT(fp, FileHasSubstr( 96 // Base64UnescapeOrDie("eW91J3JlIHRoZSBtYW4gbm93IGRvZw=="))); 97 98 class FileHasSubstrMatcher final : public ::testing::MatcherInterface<FILE*> { 99 public: 100 explicit FileHasSubstrMatcher(absl::string_view needle) : needle_(needle) {} 101 102 bool MatchAndExplain( 103 FILE* fp, ::testing::MatchResultListener* listener) const override { 104 std::string buf( 105 std::max<std::string::size_type>(needle_.size() * 2, 163840000), '\0'); 106 size_t buf_start_offset = 0; // The file offset of the byte at `buf[0]`. 107 size_t buf_data_size = 0; // The number of bytes of `buf` which contain 108 // data. 109 110 ::fseek(fp, 0, SEEK_SET); 111 while (true) { 112 // Fill the buffer to capacity or EOF: 113 while (buf_data_size < buf.size()) { 114 const size_t ret = fread(&buf[buf_data_size], sizeof(char), 115 buf.size() - buf_data_size, fp); 116 if (ret == 0) break; 117 buf_data_size += ret; 118 } 119 if (ferror(fp)) { 120 *listener << "error reading file"; 121 return false; 122 } 123 const absl::string_view haystack(&buf[0], buf_data_size); 124 const auto off = haystack.find(needle_); 125 if (off != haystack.npos) { 126 *listener << "string found at offset " << buf_start_offset + off; 127 return true; 128 } 129 if (feof(fp)) { 130 *listener << "string not found"; 131 return false; 132 } 133 // Copy the end of `buf` to the beginning so we catch matches that span 134 // buffer boundaries. `buf` and `buf_data_size` are always large enough 135 // that these ranges don't overlap. 136 memcpy(&buf[0], &buf[buf_data_size - needle_.size()], needle_.size()); 137 buf_start_offset += buf_data_size - needle_.size(); 138 buf_data_size = needle_.size(); 139 } 140 } 141 void DescribeTo(std::ostream* os) const override { 142 *os << "contains the string \"" << needle_ << "\" (base64(\"" 143 << Base64UnescapeOrDie(needle_) << "\"))"; 144 } 145 146 void DescribeNegationTo(std::ostream* os) const override { 147 *os << "does not "; 148 DescribeTo(os); 149 } 150 151 private: 152 std::string needle_; 153 }; 154 155 class StrippingTest : public ::testing::Test { 156 protected: 157 void SetUp() override { 158 #ifndef NDEBUG 159 // Non-optimized builds don't necessarily eliminate dead code at all, so we 160 // don't attempt to validate stripping against such builds. 161 GTEST_SKIP() << "StrippingTests skipped since this build is not optimized"; 162 #elif defined(__EMSCRIPTEN__) 163 // These tests require a way to examine the running binary and look for 164 // strings; there's no portable way to do that. 165 GTEST_SKIP() 166 << "StrippingTests skipped since this platform is not optimized"; 167 #endif 168 } 169 170 // Opens this program's executable file. Returns `nullptr` and writes to 171 // `stderr` on failure. 172 std::unique_ptr<FILE, std::function<void(FILE*)>> OpenTestExecutable() { 173 #if defined(__linux__) 174 std::unique_ptr<FILE, std::function<void(FILE*)>> fp( 175 fopen("/proc/self/exe", "rb"), [](FILE* fp) { fclose(fp); }); 176 if (!fp) { 177 const std::string err = absl::base_internal::StrError(errno); 178 absl::FPrintF(stderr, "Failed to open /proc/self/exe: %s\n", err); 179 } 180 return fp; 181 #elif defined(__Fuchsia__) 182 // TODO(b/242579714): We need to restore the test coverage on this platform. 183 std::unique_ptr<FILE, std::function<void(FILE*)>> fp( 184 fopen(absl::StrCat("/pkg/bin/", 185 absl::flags_internal::ShortProgramInvocationName()) 186 .c_str(), 187 "rb"), 188 [](FILE* fp) { fclose(fp); }); 189 if (!fp) { 190 const std::string err = absl::base_internal::StrError(errno); 191 absl::FPrintF(stderr, "Failed to open /pkg/bin/<binary name>: %s\n", err); 192 } 193 return fp; 194 #elif defined(__MACH__) 195 uint32_t size = 0; 196 int ret = _NSGetExecutablePath(nullptr, &size); 197 if (ret != -1) { 198 absl::FPrintF(stderr, 199 "Failed to get executable path: " 200 "_NSGetExecutablePath(nullptr) returned %d\n", 201 ret); 202 return nullptr; 203 } 204 std::string path(size, '\0'); 205 ret = _NSGetExecutablePath(&path[0], &size); 206 if (ret != 0) { 207 absl::FPrintF( 208 stderr, 209 "Failed to get executable path: _NSGetExecutablePath(buffer) " 210 "returned %d\n", 211 ret); 212 return nullptr; 213 } 214 std::unique_ptr<FILE, std::function<void(FILE*)>> fp( 215 fopen(path.c_str(), "rb"), [](FILE* fp) { fclose(fp); }); 216 if (!fp) { 217 const std::string err = absl::base_internal::StrError(errno); 218 absl::FPrintF(stderr, "Failed to open executable at %s: %s\n", path, err); 219 } 220 return fp; 221 #elif defined(_WIN32) 222 std::basic_string<TCHAR> path(4096, _T('\0')); 223 while (true) { 224 const uint32_t ret = ::GetModuleFileName(nullptr, &path[0], 225 static_cast<DWORD>(path.size())); 226 if (ret == 0) { 227 absl::FPrintF( 228 stderr, 229 "Failed to get executable path: GetModuleFileName(buffer) " 230 "returned 0\n"); 231 return nullptr; 232 } 233 if (ret < path.size()) break; 234 path.resize(path.size() * 2, _T('\0')); 235 } 236 std::unique_ptr<FILE, std::function<void(FILE*)>> fp( 237 _tfopen(path.c_str(), _T("rb")), [](FILE* fp) { fclose(fp); }); 238 if (!fp) absl::FPrintF(stderr, "Failed to open executable\n"); 239 return fp; 240 #else 241 absl::FPrintF(stderr, 242 "OpenTestExecutable() unimplemented on this platform\n"); 243 return nullptr; 244 #endif 245 } 246 247 ::testing::Matcher<FILE*> FileHasSubstr(absl::string_view needle) { 248 return MakeMatcher(new FileHasSubstrMatcher(needle)); 249 } 250 }; 251 252 // This tests whether out methodology for testing stripping works on this 253 // platform by looking for one string that definitely ought to be there and one 254 // that definitely ought not to. If this fails, none of the `StrippingTest`s 255 // are going to produce meaningful results. 256 TEST_F(StrippingTest, Control) { 257 constexpr char kEncodedPositiveControl[] = 258 "U3RyaXBwaW5nVGVzdC5Qb3NpdGl2ZUNvbnRyb2w="; 259 const std::string encoded_negative_control = 260 absl::Base64Escape("StrippingTest.NegativeControl"); 261 262 // Verify this mainly so we can encode other strings and know definitely they 263 // won't encode to `kEncodedPositiveControl`. 264 EXPECT_THAT(Base64UnescapeOrDie("U3RyaXBwaW5nVGVzdC5Qb3NpdGl2ZUNvbnRyb2w="), 265 Eq("StrippingTest.PositiveControl")); 266 267 auto exe = OpenTestExecutable(); 268 ASSERT_THAT(exe, NotNull()); 269 EXPECT_THAT(exe.get(), FileHasSubstr(kEncodedPositiveControl)); 270 EXPECT_THAT(exe.get(), Not(FileHasSubstr(encoded_negative_control))); 271 } 272 273 TEST_F(StrippingTest, Literal) { 274 // We need to load a copy of the needle string into memory (so we can search 275 // for it) without leaving it lying around in plaintext in the executable file 276 // as would happen if we used a literal. We might (or might not) leave it 277 // lying around later; that's what the tests are for! 278 const std::string needle = absl::Base64Escape("StrippingTest.Literal"); 279 LOG(INFO) << "U3RyaXBwaW5nVGVzdC5MaXRlcmFs"; 280 auto exe = OpenTestExecutable(); 281 ASSERT_THAT(exe, NotNull()); 282 if (absl::LogSeverity::kInfo >= kAbslMinLogLevel) { 283 EXPECT_THAT(exe.get(), FileHasSubstr(needle)); 284 } else { 285 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle))); 286 } 287 } 288 289 TEST_F(StrippingTest, LiteralInExpression) { 290 // We need to load a copy of the needle string into memory (so we can search 291 // for it) without leaving it lying around in plaintext in the executable file 292 // as would happen if we used a literal. We might (or might not) leave it 293 // lying around later; that's what the tests are for! 294 const std::string needle = 295 absl::Base64Escape("StrippingTest.LiteralInExpression"); 296 LOG(INFO) << absl::StrCat("secret: ", 297 "U3RyaXBwaW5nVGVzdC5MaXRlcmFsSW5FeHByZXNzaW9u"); 298 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 299 ASSERT_THAT(exe, NotNull()); 300 if (absl::LogSeverity::kInfo >= kAbslMinLogLevel) { 301 EXPECT_THAT(exe.get(), FileHasSubstr(needle)); 302 } else { 303 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle))); 304 } 305 } 306 307 TEST_F(StrippingTest, Fatal) { 308 // We need to load a copy of the needle string into memory (so we can search 309 // for it) without leaving it lying around in plaintext in the executable file 310 // as would happen if we used a literal. We might (or might not) leave it 311 // lying around later; that's what the tests are for! 312 const std::string needle = absl::Base64Escape("StrippingTest.Fatal"); 313 // We don't care if the LOG statement is actually executed, we're just 314 // checking that it's stripped. 315 if (kReallyDie) LOG(FATAL) << "U3RyaXBwaW5nVGVzdC5GYXRhbA=="; 316 317 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 318 ASSERT_THAT(exe, NotNull()); 319 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) { 320 EXPECT_THAT(exe.get(), FileHasSubstr(needle)); 321 } else { 322 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle))); 323 } 324 } 325 326 TEST_F(StrippingTest, DFatal) { 327 // We need to load a copy of the needle string into memory (so we can search 328 // for it) without leaving it lying around in plaintext in the executable file 329 // as would happen if we used a literal. We might (or might not) leave it 330 // lying around later; that's what the tests are for! 331 const std::string needle = absl::Base64Escape("StrippingTest.DFatal"); 332 // We don't care if the LOG statement is actually executed, we're just 333 // checking that it's stripped. 334 if (kReallyDie) LOG(DFATAL) << "U3RyaXBwaW5nVGVzdC5ERmF0YWw="; 335 336 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 337 ASSERT_THAT(exe, NotNull()); 338 // `DFATAL` can be `ERROR` or `FATAL`, and a compile-time optimizer doesn't 339 // know which, because `absl::kLogDebugFatal` is declared `extern` and defined 340 // in another TU. Link-time optimization might do better. We have six cases: 341 // | `AMLL` is-> | `<=ERROR` | `FATAL` | `>FATAL` | 342 // | ------------------- | --------- | ------- | -------- | 343 // | `DFATAL` is `ERROR` | present | ? | stripped | 344 // | `DFATAL` is `FATAL` | present | present | stripped | 345 346 // These constexpr variables are used to suppress unreachable code warnings 347 // in the if-else statements below. 348 349 // "present" in the table above: `DFATAL` exceeds `ABSL_MIN_LOG_LEVEL`, so 350 // `DFATAL` statements should not be stripped (and they should be logged 351 // when executed, but that's a different testsuite). 352 constexpr bool kExpectPresent = absl::kLogDebugFatal >= kAbslMinLogLevel; 353 354 // "stripped" in the table above: even though the compiler may not know 355 // which value `DFATAL` has, it should be able to strip it since both 356 // possible values ought to be stripped. 357 constexpr bool kExpectStripped = kAbslMinLogLevel > absl::LogSeverity::kFatal; 358 359 if (kExpectPresent) { 360 EXPECT_THAT(exe.get(), FileHasSubstr(needle)); 361 } else if (kExpectStripped) { 362 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle))); 363 } else { 364 // "?" in the table above; may or may not be stripped depending on whether 365 // any link-time optimization is done. Either outcome is ok. 366 } 367 } 368 369 TEST_F(StrippingTest, Level) { 370 const std::string needle = absl::Base64Escape("StrippingTest.Level"); 371 volatile auto severity = absl::LogSeverity::kWarning; 372 // Ensure that `severity` is not a compile-time constant to prove that 373 // stripping works regardless: 374 LOG(LEVEL(severity)) << "U3RyaXBwaW5nVGVzdC5MZXZlbA=="; 375 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 376 ASSERT_THAT(exe, NotNull()); 377 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) { 378 // This can't be stripped at compile-time because it might evaluate to a 379 // level that shouldn't be stripped. 380 EXPECT_THAT(exe.get(), FileHasSubstr(needle)); 381 } else { 382 #if (defined(_MSC_VER) && !defined(__clang__)) || defined(__APPLE__) 383 // Dead code elimination misses this case. 384 #else 385 // All levels should be stripped, so it doesn't matter what the severity 386 // winds up being. 387 EXPECT_THAT(exe.get(), Not(FileHasSubstr(needle))); 388 #endif 389 } 390 } 391 392 TEST_F(StrippingTest, Check) { 393 // Here we also need a variable name with enough entropy that it's unlikely to 394 // appear in the binary by chance. `volatile` keeps the tautological 395 // comparison (and the rest of the `CHECK`) from being optimized away. 396 const std::string var_needle = absl::Base64Escape("StrippingTestCheckVar"); 397 const std::string msg_needle = absl::Base64Escape("StrippingTest.Check"); 398 volatile int U3RyaXBwaW5nVGVzdENoZWNrVmFy = 0xCAFE; 399 // We don't care if the CHECK is actually executed, just that stripping works. 400 // Hiding it behind `kReallyDie` works around some overly aggressive 401 // optimizations in older versions of MSVC. 402 if (kReallyDie) { 403 CHECK(U3RyaXBwaW5nVGVzdENoZWNrVmFy != U3RyaXBwaW5nVGVzdENoZWNrVmFy) 404 << "U3RyaXBwaW5nVGVzdC5DaGVjaw=="; 405 } 406 407 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 408 ASSERT_THAT(exe, NotNull()); 409 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) { 410 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle)); 411 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle)); 412 } else { 413 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle))); 414 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle))); 415 } 416 } 417 418 TEST_F(StrippingTest, CheckOp) { 419 // See `StrippingTest.Check` for some hairy implementation notes. 420 const std::string var_needle1 = 421 absl::Base64Escape("StrippingTestCheckOpVar1"); 422 const std::string var_needle2 = 423 absl::Base64Escape("StrippingTestCheckOpVar2"); 424 const std::string msg_needle = absl::Base64Escape("StrippingTest.CheckOp"); 425 volatile int U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIx = 0xFEED; 426 volatile int U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIy = 0xCAFE; 427 if (kReallyDie) { 428 CHECK_EQ(U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIx, U3RyaXBwaW5nVGVzdENoZWNrT3BWYXIy) 429 << "U3RyaXBwaW5nVGVzdC5DaGVja09w"; 430 } 431 432 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 433 ASSERT_THAT(exe, NotNull()); 434 435 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) { 436 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle1)); 437 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle2)); 438 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle)); 439 } else { 440 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle1))); 441 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle2))); 442 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle))); 443 } 444 } 445 446 TEST_F(StrippingTest, CheckStrOp) { 447 // See `StrippingTest.Check` for some hairy implementation notes. 448 const std::string var_needle1 = 449 absl::Base64Escape("StrippingTestCheckStrOpVar1"); 450 const std::string var_needle2 = 451 absl::Base64Escape("StrippingTestCheckStrOpVar2"); 452 const std::string msg_needle = absl::Base64Escape("StrippingTest.CheckStrOp"); 453 const char *volatile U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIx = "FEED"; 454 const char *volatile U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIy = "CAFE"; 455 if (kReallyDie) { 456 CHECK_STREQ(U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIx, 457 U3RyaXBwaW5nVGVzdENoZWNrU3RyT3BWYXIy) 458 << "U3RyaXBwaW5nVGVzdC5DaGVja1N0ck9w"; 459 } 460 461 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 462 ASSERT_THAT(exe, NotNull()); 463 464 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) { 465 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle1)); 466 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle2)); 467 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle)); 468 } else { 469 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle1))); 470 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle2))); 471 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle))); 472 } 473 } 474 475 TEST_F(StrippingTest, CheckOk) { 476 // See `StrippingTest.Check` for some hairy implementation notes. 477 const std::string var_needle = absl::Base64Escape("StrippingTestCheckOkVar1"); 478 const std::string msg_needle = absl::Base64Escape("StrippingTest.CheckOk"); 479 volatile bool x = false; 480 auto U3RyaXBwaW5nVGVzdENoZWNrT2tWYXIx = absl::OkStatus(); 481 if (x) { 482 U3RyaXBwaW5nVGVzdENoZWNrT2tWYXIx = 483 absl::InvalidArgumentError("Stripping this is not my job!"); 484 } 485 if (kReallyDie) { 486 CHECK_OK(U3RyaXBwaW5nVGVzdENoZWNrT2tWYXIx) 487 << "U3RyaXBwaW5nVGVzdC5DaGVja09r"; 488 } 489 490 std::unique_ptr<FILE, std::function<void(FILE*)>> exe = OpenTestExecutable(); 491 ASSERT_THAT(exe, NotNull()); 492 493 if (absl::LogSeverity::kFatal >= kAbslMinLogLevel) { 494 EXPECT_THAT(exe.get(), FileHasSubstr(var_needle)); 495 EXPECT_THAT(exe.get(), FileHasSubstr(msg_needle)); 496 } else { 497 EXPECT_THAT(exe.get(), Not(FileHasSubstr(var_needle))); 498 EXPECT_THAT(exe.get(), Not(FileHasSubstr(msg_needle))); 499 } 500 } 501 502 } // namespace