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