From 118907f6b937198347a6ae559100baad4730ca36 Mon Sep 17 00:00:00 2001 From: Chris Kennelly Date: Mon, 20 Jul 2026 12:12:27 -0700 Subject: [PATCH] Extract fuzz test-like CPU set roundtrip test into a proper fuzztest. This gives us access to the best aspects of the fuzzer engine, rather than what absl::BitGen might happen to bash on. PiperOrigin-RevId: 950981111 --- tcmalloc/internal/BUILD | 1 + tcmalloc/internal/sysinfo_fuzz.cc | 74 +++++++++++++++++++++++++++++++ tcmalloc/internal/sysinfo_test.cc | 62 -------------------------- 3 files changed, 75 insertions(+), 62 deletions(-) diff --git a/tcmalloc/internal/BUILD b/tcmalloc/internal/BUILD index d67b8ac3e..409ce35b2 100644 --- a/tcmalloc/internal/BUILD +++ b/tcmalloc/internal/BUILD @@ -1331,6 +1331,7 @@ cc_test( deps = [ ":cpu_utils", ":sysinfo", + "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:string_view", "@com_google_fuzztest//fuzztest", "@com_google_fuzztest//fuzztest:fuzztest_gtest_main", diff --git a/tcmalloc/internal/sysinfo_fuzz.cc b/tcmalloc/internal/sysinfo_fuzz.cc index 740515473..8c9e11ce4 100644 --- a/tcmalloc/internal/sysinfo_fuzz.cc +++ b/tcmalloc/internal/sysinfo_fuzz.cc @@ -16,8 +16,14 @@ #include #include #include +#include +#include +#include "gmock/gmock.h" +#include "gtest/gtest.h" #include "fuzztest/fuzztest.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "tcmalloc/internal/cpu_utils.h" #include "tcmalloc/internal/sysinfo.h" @@ -44,5 +50,73 @@ void ParseInput(absl::string_view s) { FUZZ_TEST(SysinfoTest, ParseInput); +void FuzzCpuSetRoundTrip(const std::vector& cpus_to_set, + const std::vector& read_sizes) { + CpuSet reference; + reference.Zero(); + + // Serialize the reference set into a cpulist-style string. + for (int cpu : cpus_to_set) { + if (cpu >= 0 && cpu < kMaxCpus) { + reference.Set(cpu); + } + } + + std::vector components; + for (int cpu = 0; cpu < kMaxCpus; cpu++) { + if (!reference.IsSet(cpu)) continue; + + const int start = cpu; + int next = cpu + 1; + while (next < kMaxCpus && reference.IsSet(next)) { + cpu = next; + next = cpu + 1; + } + + if (cpu == start) { + components.push_back(absl::StrCat(cpu)); + } else { + components.push_back(absl::StrCat(start, "-", cpu)); + } + } + const std::string serialized = absl::StrJoin(components, ","); + + absl::string_view remaining(serialized); + // Now parse that string using our ParseCpulist function, randomizing the + // amount of data we provide to it from each read. + size_t read_sizes_idx = 0; + + const std::optional parsed = + ParseCpulist([&](char* const buf, const size_t count) -> ssize_t { + // Calculate how much data we have left to provide. + const size_t max = std::min(count, remaining.size()); + + // If none, we have no choice but to provide nothing. + if (max == 0) return 0; + + // If we do have data, return a randomly sized subset of it to stress + // the logic around reading partial values. + size_t copy; + if (read_sizes_idx < read_sizes.size()) { + copy = (read_sizes[read_sizes_idx] % max) + 1; + read_sizes_idx++; + } else { + copy = max; + } + + memcpy(buf, remaining.data(), copy); + remaining.remove_prefix(copy); + return copy; + }); + + // We ought to have parsed the same set of CPUs that we serialized. + ASSERT_THAT(parsed, testing::Ne(std::nullopt)); + EXPECT_TRUE(CPU_EQUAL_S(kCpuSetBytes, parsed->data(), reference.data())); +} + +FUZZ_TEST(SysinfoTest, FuzzCpuSetRoundTrip) + .WithDomains(fuzztest::VectorOf(fuzztest::InRange(0, kMaxCpus - 1)), + fuzztest::VectorOf(fuzztest::Arbitrary())); + } // namespace } // namespace tcmalloc::tcmalloc_internal diff --git a/tcmalloc/internal/sysinfo_test.cc b/tcmalloc/internal/sysinfo_test.cc index 950f6f344..d4470d3ee 100644 --- a/tcmalloc/internal/sysinfo_test.cc +++ b/tcmalloc/internal/sysinfo_test.cc @@ -99,68 +99,6 @@ TEST(ParseCpulistTest, NotInBounds) { ASSERT_THAT(parsed, testing::Eq(std::nullopt)); } -// Ensure that we can parse randomized cpulists correctly. -TEST(ParseCpulistTest, Random) { - absl::BitGen gen; - - static constexpr int kIterations = 100; - for (int i = 0; i < kIterations; i++) { - CpuSet reference; - reference.Zero(); - - // Set a random number of CPUs within the reference set. - const double density = absl::Uniform(gen, 0.0, 1.0); - for (int cpu = 0; cpu < kMaxCpus; cpu++) { - if (absl::Bernoulli(gen, density)) { - reference.Set(cpu); - } - } - - // Serialize the reference set into a cpulist-style string. - std::vector components; - for (int cpu = 0; cpu < kMaxCpus; cpu++) { - if (!reference.IsSet(cpu)) continue; - - const int start = cpu; - int next = cpu + 1; - while (next < kMaxCpus && reference.IsSet(next)) { - cpu = next; - next = cpu + 1; - } - - if (cpu == start) { - components.push_back(absl::StrCat(cpu)); - } else { - components.push_back(absl::StrCat(start, "-", cpu)); - } - } - const std::string serialized = absl::StrJoin(components, ","); - - // Now parse that string using our ParseCpulist function, randomizing the - // amount of data we provide to it from each read. - absl::string_view remaining(serialized); - const absl::optional parsed = - ParseCpulist([&](char* const buf, const size_t count) -> ssize_t { - // Calculate how much data we have left to provide. - const size_t max = std::min(count, remaining.size()); - - // If none, we have no choice but to provide nothing. - if (max == 0) return 0; - - // If we do have data, return a randomly sized subset of it to stress - // the logic around reading partial values. - const size_t copy = absl::Uniform(gen, static_cast(1), max); - memcpy(buf, remaining.data(), copy); - remaining.remove_prefix(copy); - return copy; - }); - - // We ought to have parsed the same set of CPUs that we serialized. - ASSERT_THAT(parsed, testing::Ne(std::nullopt)); - EXPECT_TRUE(CPU_EQUAL_S(kCpuSetBytes, parsed->data(), reference.data())); - } -} - TEST(NumCPUs, NoCache) { const int result = []() { AllocationGuard guard;