diff --git a/.bazelrc b/.bazelrc index 4d727275..a6149e87 100644 --- a/.bazelrc +++ b/.bazelrc @@ -35,6 +35,8 @@ build:linux --copt=-mcrc32 build --define absl=1 build:macos --macos_minimum_os=10.15 +# For compatible behavior on weak symbols. +build:macos --linkopt=-Wl,-undefined,dynamic_lookup # To create this file, please run: # diff --git a/.github/workflows/bazel_test_centipede.yml b/.github/workflows/bazel_test_centipede.yml index 87640271..3464bdf3 100644 --- a/.github/workflows/bazel_test_centipede.yml +++ b/.github/workflows/bazel_test_centipede.yml @@ -107,29 +107,22 @@ jobs: - name: Run unit tests if: ${{ !cancelled() }} run: | - bazel test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli \ - centipede:binary_info_test centipede:byte_array_mutator_test \ - centipede:call_graph_test centipede:callstack_test \ - centipede:centipede_binary_test centipede:concurrent_bitset_test \ - centipede:command_test \ - centipede:concurrent_byteset_test centipede:config_file_test \ - centipede:config_util_test centipede:corpus_io_test \ - centipede:corpus_test centipede:distill_test \ - centipede:environment_test centipede:execution_metadata_test \ - centipede:feature_set_test centipede:feature_test \ - centipede:foreach_nonzero_test \ - centipede:hashed_ring_buffer_test centipede:int_utils_test \ - centipede:knobs_test centipede:mutation_input_test \ - centipede:pc_info_test centipede:resource_pool_test \ - centipede:reverse_pc_table_test centipede:rolling_hash_test \ - centipede:runner_cmp_trace_test centipede:runner_flags_test \ - centipede:runner_result_test centipede:rusage_stats_test \ - centipede:seed_corpus_maker_lib_test \ - centipede:seed_corpus_maker_proto_lib_test \ - centipede:shared_memory_blob_sequence_test \ - centipede:stats_test centipede:symbol_table_test \ - centipede:util_test centipede:workdir_test && \ + # Timed wait is delayed for unknown reasons in MacOS for a few testing + # environments, thus disabling a few related non-critical tests. + bazel test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli -- centipede:all -centipede:periodic_action_test -centipede:rusage_profiler_test && \ bazel test --local_test_jobs=1 --test_output=errors --no//fuzztest:use_riegeli --test_filter='-*ValidateTimelapseSnapshots' -- centipede:rusage_profiler_test + - name: Run e2e tests without Riegeli + if: ${{ !cancelled() }} + run: | + bazel test --test_output=errors --no//fuzztest:use_riegeli centipede/testing:instrumentation_test centipede/testing:runner_test centipede/testing:multi_dso_test + - name: Run puzzles without Riegeli + if: ${{ !cancelled() }} + run: | + bazel test --test_output=errors --no//fuzztest:use_riegeli centipede/puzzles:all + - name: Run puzzles without Riegeli with ASAN + if: ${{ !cancelled() }} + run: | + bazel test --test_output=errors --no//fuzztest:use_riegeli --linkopt=-fsanitize=address --copt=-fsanitize=address --platform_suffix=asan centipede/puzzles:all - name: Save new cache based on main if: github.ref == 'refs/heads/main' uses: actions/cache/save@v4 diff --git a/centipede/BUILD b/centipede/BUILD index 7669751b..def96cd5 100644 --- a/centipede/BUILD +++ b/centipede/BUILD @@ -1032,9 +1032,13 @@ RUNNER_SANITIZED_COPTS = [ RUNNER_LINKOPTS = [ "-ldl", # for dlsym - "-lrt", # for shm_open "-lpthread", # for pthread_once -] +] + select({ + "@platforms//os:macos": [], + "//conditions:default": [ + "-lrt", # for shm_open + ], +}) # WARNING: be careful with more deps here. Use only the most trivial ones. RUNNER_DEPS = [ @@ -1200,8 +1204,6 @@ sh_library( sh_library( name = "test_util_sh", srcs = ["test_util.sh"], - data = [ - ], ) ################################################################################ diff --git a/centipede/centipede_callbacks.h b/centipede/centipede_callbacks.h index 628dd969..264dbc2a 100644 --- a/centipede/centipede_callbacks.h +++ b/centipede/centipede_callbacks.h @@ -176,8 +176,8 @@ class CentipedeCallbacks { std::filesystem::path(temp_dir_).append("log"); std::string failure_description_path_ = std::filesystem::path(temp_dir_).append("failure_description"); - const std::string shmem_name1_ = ProcessAndThreadUniqueID("/centipede-shm1-"); - const std::string shmem_name2_ = ProcessAndThreadUniqueID("/centipede-shm2-"); + const std::string shmem_name1_ = ProcessAndThreadUniqueID("/ctpd-shm1-"); + const std::string shmem_name2_ = ProcessAndThreadUniqueID("/ctpd-shm2-"); SharedMemoryBlobSequence inputs_blobseq_; SharedMemoryBlobSequence outputs_blobseq_; diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc index fe856072..e9a887c5 100644 --- a/centipede/centipede_test.cc +++ b/centipede/centipede_test.cc @@ -175,23 +175,29 @@ TEST(Centipede, ReadFirstCorpusDir) { env.require_pc_table = false; // No PC table here. env.corpus_dir.push_back(corpus_dir.path()); - // First, generate corpus files in corpus_dir. - CentipedeMock mock_1(env); - MockFactory factory_1(mock_1); - CentipedeMain(env, factory_1); - ASSERT_EQ(mock_1.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. - ASSERT_EQ(mock_1.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. - ASSERT_EQ(CountFilesInDir(env.corpus_dir[0]), - 512); // All 1-byte and 2-byte inputs. - - // Second, run without fuzzing using the same corpus_dir. - env.workdir = workdir_2.path(); - env.num_runs = 0; - CentipedeMock mock_2(env); - MockFactory factory_2(mock_2); - CentipedeMain(env, factory_2); - // Should observe all inputs in corpus_dir. - EXPECT_EQ(mock_2.num_inputs_, 512); + // Need to wrap each CentipedeMain in a scope to make sure the shmem is + // released before the next call. Otherwise it may fail in MacOS. + { + // First, generate corpus files in corpus_dir. + CentipedeMock mock_1(env); + MockFactory factory_1(mock_1); + CentipedeMain(env, factory_1); + ASSERT_EQ(mock_1.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. + ASSERT_EQ(mock_1.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. + ASSERT_EQ(CountFilesInDir(env.corpus_dir[0]), + 512); // All 1-byte and 2-byte inputs. + } + + { + // Second, run without fuzzing using the same corpus_dir. + env.workdir = workdir_2.path(); + env.num_runs = 0; + CentipedeMock mock_2(env); + MockFactory factory_2(mock_2); + CentipedeMain(env, factory_2); + // Should observe all inputs in corpus_dir. + EXPECT_EQ(mock_2.num_inputs_, 512); + } } TEST(Centipede, DoesNotReadFirstCorpusDirIfOutputOnly) { @@ -206,24 +212,29 @@ TEST(Centipede, DoesNotReadFirstCorpusDirIfOutputOnly) { env.require_pc_table = false; // No PC table here. env.corpus_dir.push_back(corpus_dir.path()); - // First, generate corpus files in corpus_dir. - CentipedeMock mock_1(env); - MockFactory factory_1(mock_1); - CentipedeMain(env, factory_1); - ASSERT_EQ(mock_1.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. - ASSERT_EQ(mock_1.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. - ASSERT_EQ(CountFilesInDir(env.corpus_dir[0]), - 512); // All 1-byte and 2-byte inputs. - - // Second, run without fuzzing using the same corpus_dir, but as output-only. - env.workdir = workdir_2.path(); - env.num_runs = 0; - env.first_corpus_dir_output_only = true; - CentipedeMock mock_2(env); - MockFactory factory_2(mock_2); - CentipedeMain(env, factory_2); - // Should observe no inputs other than the seed input {0}. - EXPECT_EQ(mock_2.num_inputs_, 1); + { + // First, generate corpus files in corpus_dir. + CentipedeMock mock_1(env); + MockFactory factory_1(mock_1); + CentipedeMain(env, factory_1); + ASSERT_EQ(mock_1.observed_1byte_inputs_.size(), 256); // all 1-byte seqs. + ASSERT_EQ(mock_1.observed_2byte_inputs_.size(), 65536); // all 2-byte seqs. + ASSERT_EQ(CountFilesInDir(env.corpus_dir[0]), + 512); // All 1-byte and 2-byte inputs. + } + + { + // Second, run without fuzzing using the same corpus_dir, but as + // output-only. + env.workdir = workdir_2.path(); + env.num_runs = 0; + env.first_corpus_dir_output_only = true; + CentipedeMock mock_2(env); + MockFactory factory_2(mock_2); + CentipedeMain(env, factory_2); + // Should observe no inputs other than the seed input {0}. + EXPECT_EQ(mock_2.num_inputs_, 1); + } } TEST(Centipede, SkipsOutputIfFirstCorpusDirIsEmptyPath) { @@ -834,31 +845,32 @@ TEST(Centipede, UndetectedCrashingInput) { env.require_pc_table = false; env.exit_on_crash = true; - UndetectedCrashingInputMock mock(env, kCrashingInputIdx); - MockFactory factory(mock); - CentipedeMain(env, factory); + { + UndetectedCrashingInputMock mock(env, kCrashingInputIdx); + MockFactory factory(mock); + CentipedeMain(env, factory); - // Verify that we see the expected inputs from the batch. - // The "crashes/unreliable_batch-" dir must contain all inputs from the - // batch that were executing during the session. - // We simply verify the number of saved inputs matches the number of executed - // inputs. - const auto crashing_input_hash = Hash(mock.crashing_input()); - const auto crashes_dir_path = std::filesystem::path(temp_dir.path()) - .append("crashes") - .append("crashing_batch-") - .concat(crashing_input_hash); - EXPECT_TRUE(std::filesystem::exists(crashes_dir_path)) << crashes_dir_path; - std::vector found_crash_file_names; - for (auto const &dir_ent : - std::filesystem::directory_iterator(crashes_dir_path)) { - found_crash_file_names.push_back(dir_ent.path().filename()); - } - // TODO(ussuri): Verify exact names/contents of the files, not just count. - EXPECT_EQ(found_crash_file_names.size(), kCrashingInputIdxInBatch + 1); - // Suspected input first, then every input in the batch (including the - // suspected input again). - EXPECT_EQ(mock.num_inputs_triaged(), kBatchSize + 1); + // Verify that we see the expected inputs from the batch. + // The "crashes/unreliable_batch-" dir must contain all inputs from + // the batch that were executing during the session. We simply verify the + // number of saved inputs matches the number of executed inputs. + const auto crashing_input_hash = Hash(mock.crashing_input()); + const auto crashes_dir_path = std::filesystem::path(temp_dir.path()) + .append("crashes") + .append("crashing_batch-") + .concat(crashing_input_hash); + EXPECT_TRUE(std::filesystem::exists(crashes_dir_path)) << crashes_dir_path; + std::vector found_crash_file_names; + for (auto const &dir_ent : + std::filesystem::directory_iterator(crashes_dir_path)) { + found_crash_file_names.push_back(dir_ent.path().filename()); + } + // TODO(ussuri): Verify exact names/contents of the files, not just count. + EXPECT_EQ(found_crash_file_names.size(), kCrashingInputIdxInBatch + 1); + // Suspected input first, then every input in the batch (including the + // suspected input again). + EXPECT_EQ(mock.num_inputs_triaged(), kBatchSize + 1); + } // Verify that when `env.batch_triage_suspect_only` is set, only triage the // suspect. diff --git a/centipede/control_flow.cc b/centipede/control_flow.cc index 5b28d2d6..78ebb9aa 100644 --- a/centipede/control_flow.cc +++ b/centipede/control_flow.cc @@ -87,8 +87,10 @@ PCTable GetPcTableFromBinaryWithTracePC(std::string_view binary_path, saw_new_function = true; continue; } - if (!absl::EndsWith(line, "<__sanitizer_cov_trace_pc>") && - !absl::EndsWith(line, "<__sanitizer_cov_trace_pc@plt>")) + // On MacOS there is an extra underscope before the symbols, so not sealing + // the symbol with `<`. + if (!absl::EndsWith(line, "__sanitizer_cov_trace_pc>") && + !absl::EndsWith(line, "__sanitizer_cov_trace_pc@plt>")) continue; uintptr_t pc = std::stoul(line, nullptr, 16); uintptr_t flags = saw_new_function ? PCInfo::kFuncEntry : 0; diff --git a/centipede/control_flow_test.cc b/centipede/control_flow_test.cc index e5af0fc0..03171cb6 100644 --- a/centipede/control_flow_test.cc +++ b/centipede/control_flow_test.cc @@ -314,7 +314,7 @@ static void SymbolizeBinary(std::string_view test_dir, has_llvm_fuzzer_test_one_input = true; EXPECT_THAT( symbols.location(i), - testing::HasSubstr("centipede/testing/test_fuzz_target.cc:70")); + testing::HasSubstr("centipede/testing/test_fuzz_target.cc:71")); } } EXPECT_TRUE(has_llvm_fuzzer_test_one_input); diff --git a/centipede/environment.h b/centipede/environment.h index a2b25002..d21392f5 100644 --- a/centipede/environment.h +++ b/centipede/environment.h @@ -54,7 +54,13 @@ struct Environment { bool serialize_shard_loads = false; size_t seed = 0; size_t prune_frequency = 100; +#ifdef __APPLE__ + // Address space limit is ignored on MacOS. + // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=853873#c2 + size_t address_space_limit_mb = 0; +#else // __APPLE__ size_t address_space_limit_mb = 8192; +#endif // __APPLE__ size_t rss_limit_mb = 4096; size_t timeout_per_input = 60; size_t timeout_per_batch = 0; diff --git a/centipede/runner.cc b/centipede/runner.cc index bfec569f..8fd9c649 100644 --- a/centipede/runner.cc +++ b/centipede/runner.cc @@ -23,8 +23,6 @@ #include "./centipede/runner.h" #include // NOLINT: use pthread to avoid extra dependencies. -#include -#include #include #include #include @@ -95,10 +93,6 @@ thread_local ThreadTerminationDetector termination_detector; } // namespace -// Use of the fixed init priority allows to call CentipedeRunnerMain -// from constructor functions (CentipedeRunnerMain needs to run after -// state constructor). -// Note: it must run after ForkServerCallMeVeryEarly, see comment there. GlobalRunnerState state __attribute__((init_priority(200))); // We use __thread instead of thread_local so that the compiler warns if // the initializer for `tls` is not a constant expression. @@ -193,8 +187,14 @@ void ThreadLocalRunnerState::OnThreadStop() { static size_t GetPeakRSSMb() { struct rusage usage = {}; if (getrusage(RUSAGE_SELF, &usage) != 0) return 0; +#ifdef __APPLE__ + // On MacOS, the unit seems to be byte according to experiment, while some + // documents mentioned KiB. This could depend on OS variants. + return usage.ru_maxrss >> 20; +#else // __APPLE__ // On Linux, ru_maxrss is in KiB return usage.ru_maxrss >> 10; +#endif // __APPLE__ } // Returns the current time in microseconds. @@ -281,7 +281,8 @@ __attribute__((noinline)) void CheckStackLimit(uintptr_t sp) { tls.top_frame_sp - sp > stack_limit) { if (stack_limit_exceeded.test_and_set()) return; fprintf(stderr, - "========= Stack limit exceeded: %" PRIuPTR " > %" PRIu64 + "========= Stack limit exceeded: %" PRIuPTR + " > %zu" " (byte); aborting\n", tls.top_frame_sp - sp, stack_limit); centipede::WriteFailureDescription( @@ -951,13 +952,17 @@ static size_t GetVmSizeInBytes() { // NOTE: Ignore any (unlikely) failures to suppress a compiler warning. (void)fscanf(f, "%zd", &vm_size); fclose(f); - return vm_size * getauxval(AT_PAGESZ); // proc gives VmSize in pages. + return vm_size * getpagesize(); // proc gives VmSize in pages. } // Sets RLIMIT_CORE, RLIMIT_AS static void SetLimits() { - // no core files anywhere. - prctl(PR_SET_DUMPABLE, 0); + // Disable core dumping. + struct rlimit core_limits; + getrlimit(RLIMIT_CORE, &core_limits); + core_limits.rlim_cur = 0; + core_limits.rlim_max = 0; + setrlimit(RLIMIT_CORE, &core_limits); // ASAN/TSAN/MSAN can not be used with RLIMIT_AS. // We get the current VmSize, if it is greater than 1Tb, we assume we @@ -1013,6 +1018,9 @@ extern void RunnerInterceptor(); &RunnerInterceptor; GlobalRunnerState::GlobalRunnerState() { + // Make sure fork server is started if needed. + ForkServerCallMeVeryEarly(); + // TODO(kcc): move some code from CentipedeRunnerMain() here so that it works // even if CentipedeRunnerMain() is not called. tls.OnThreadStart(); diff --git a/centipede/runner.h b/centipede/runner.h index 4a4440d6..329f580c 100644 --- a/centipede/runner.h +++ b/centipede/runner.h @@ -99,7 +99,7 @@ struct ThreadLocalRunnerState { // Paths are thread-local, so we maintain the current bounded path here. // We allow paths of up to 100, controlled at run-time via the "path_level". - static constexpr size_t kBoundedPathLength = 100; + static constexpr uint64_t kBoundedPathLength = 100; HashedRingBuffer path_ring_buffer; // Value of SP in the top call frame of the thread, computed in OnThreadStart. diff --git a/centipede/runner_dl_info.cc b/centipede/runner_dl_info.cc index 4e8a3a18..c8967945 100644 --- a/centipede/runner_dl_info.cc +++ b/centipede/runner_dl_info.cc @@ -14,8 +14,13 @@ #include "./centipede/runner_dl_info.h" +#ifdef __APPLE__ +#include +#include +#else // __APPLE__ #include #include // dl_iterate_phdr +#endif // __APPLE__ #include #include @@ -31,6 +36,153 @@ namespace centipede { namespace { +constexpr bool kDlDebug = false; // we may want to make it a runtime flag. + +bool StringEndsWithSuffix(absl::Nonnull string, + absl::Nonnull suffix) { + const char* pos = std::strstr(string, suffix); + if (pos == nullptr) return false; + return pos == string + std::strlen(string) - std::strlen(suffix); +} + +} // namespace + +#ifdef __APPLE__ +// Reference: +// https://opensource.apple.com/source/xnu/xnu-4903.221.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html + +namespace { + +// Calls `callback` on the segments with the link-time start +// address and size. +void FindSegment(const mach_header* header, + const std::function& callback) { + const load_command* cmd = nullptr; + if (header->magic == MH_MAGIC) { + cmd = reinterpret_cast( + reinterpret_cast(header) + sizeof(mach_header)); + } else if (header->magic == MH_MAGIC_64) { + cmd = reinterpret_cast( + reinterpret_cast(header) + sizeof(mach_header_64)); + } + RunnerCheck(cmd != nullptr, "bad magic number of mach image header"); + for (size_t cmd_index = 0; cmd_index < header->ncmds; + ++cmd_index, cmd = reinterpret_cast( + reinterpret_cast(cmd) + cmd->cmdsize)) { + if constexpr (kDlDebug) { + fprintf(stderr, "%s command at %p with size 0x%" PRIx32 "\n", __func__, + cmd, cmd->cmdsize); + } + uintptr_t base, size; + const char* name; + if (cmd->cmd == LC_SEGMENT) { + const auto* seg = reinterpret_cast(cmd); + base = seg->vmaddr; + size = seg->vmsize; + name = seg->segname; + } else if (cmd->cmd == LC_SEGMENT_64) { + const auto* seg = reinterpret_cast(cmd); + base = seg->vmaddr; + size = seg->vmsize; + name = seg->segname; + } else { + continue; + } + if constexpr (kDlDebug) { + fprintf(stderr, + "%s segment name %s addr seg 0x%" PRIxPTR " size 0x%" PRIxPTR + "\n", + __func__, name, base, size); + } + if (std::strcmp(name, "__PAGEZERO") == 0) continue; + callback(name, base, size); + } + if constexpr (kDlDebug) { + fprintf(stderr, "%s finished\n", __func__); + } +} + +DlInfo GetDlInfoFromImage( + const std::function& + image_filter) { + DlInfo result; + result.Clear(); + const auto image_count = _dyld_image_count(); + for (uint32_t i = 0; i < image_count; ++i) { + const mach_header* header = _dyld_get_image_header(i); + RunnerCheck(header != nullptr, "failed to get image header"); + const char* name = _dyld_get_image_name(i); + RunnerCheck(name != nullptr, "bad image name"); + if constexpr (kDlDebug) { + fprintf(stderr, "%s image header at %p, name %s\n", __func__, header, + name); + } + if (!image_filter(header, name)) continue; + uintptr_t image_start = 0; + uintptr_t image_end = 0; + FindSegment(header, + [&image_start, &image_end](const char* unused_segment_name, + uintptr_t start, uintptr_t size) { + if (image_end == 0) image_start = start; + image_end = std::max(image_end, start + size); + }); + result.link_offset = _dyld_get_image_vmaddr_slide(i); + result.start_address = image_start + result.link_offset; + result.size = image_end - image_start; + RunnerCheck(result.size > 0, "bad image size"); + std::strncpy(result.path, name, sizeof(result.path)); + break; + } + if constexpr (kDlDebug) { + fprintf(stderr, "%s succeeded? %d\n", __func__, result.IsSet()); + if (result.IsSet()) { + fprintf(stderr, + " start 0x%" PRIxPTR " size 0x%" PRIxPTR + " link_offset 0x%" PRIxPTR "\n", + result.start_address, result.size, result.link_offset); + } + } + return result; +} + +} // namespace + +DlInfo GetDlInfo(absl::Nullable dl_path_suffix) { + if constexpr (kDlDebug) { + fprintf(stderr, "GetDlInfo for path suffix %s\n", + dl_path_suffix ? dl_path_suffix : "(null)"); + } + return GetDlInfoFromImage( + [dl_path_suffix](const mach_header* unused_header, const char* name) { + return dl_path_suffix == nullptr || + StringEndsWithSuffix(name, dl_path_suffix); + }); +} + +DlInfo GetDlInfo(uintptr_t pc) { + if constexpr (kDlDebug) { + fprintf(stderr, "GetDlInfo for pc 0x%" PRIxPTR "\n", pc); + } + return GetDlInfoFromImage([pc](const mach_header* header, + const char* unused_image_name) { + bool matched = false; + FindSegment(header, [pc, header, &matched]( + const char* name, uintptr_t start, uintptr_t size) { + if (std::strcmp(name, "__TEXT") != 0) return; + const uintptr_t runtime_text_start = reinterpret_cast(header); + if (pc >= runtime_text_start && pc < runtime_text_start + size) { + matched = true; + } + }); + return matched; + }); +} + +#else // __APPLE__ + +namespace { + // Struct to pass to dl_iterate_phdr's callback. struct DlCallbackParam { // Full path to the instrumented library or nullptr for the main binary. @@ -41,17 +193,8 @@ struct DlCallbackParam { DlInfo &result; }; -bool StringEndsWithSuffix(const char *string, - absl::Nonnull suffix) { - const char *pos = strstr(string, suffix); - if (pos == nullptr) return false; - return pos == string + strlen(string) - strlen(suffix); -} - int g_some_global; // Used in DlIteratePhdrCallback. -constexpr bool kDlDebug = false; // we may want to make it a runtime flag. - // Returns the size of the DL represented by `info`. size_t DlSize(absl::Nonnull info) { size_t size = 0; @@ -107,8 +250,9 @@ int DlIteratePhdrCallback(absl::Nonnull info, result.start_address = info->dlpi_addr; result.size = DlSize(info); + result.link_offset = result.start_address; // copy dlpi_name to result.path. - strncpy(result.path, info->dlpi_name, sizeof(result.path)); + std::strncpy(result.path, info->dlpi_name, sizeof(result.path)); result.path[sizeof(result.path) - 1] = 0; if constexpr (kDlDebug) { @@ -150,9 +294,10 @@ int DlIteratePhdrPCCallback(absl::Nonnull info, if (param->pc >= info->dlpi_addr + size) return 0; // wrong DSO. result.start_address = info->dlpi_addr; result.size = size; - if (strlen(info->dlpi_name) != 0) { + result.link_offset = result.start_address; + if (std::strlen(info->dlpi_name) != 0) { // copy dlpi_name to result.path. - strncpy(result.path, info->dlpi_name, sizeof(result.path)); + std::strncpy(result.path, info->dlpi_name, sizeof(result.path)); } else { // dlpi_name is empty, this is the main binary, get path via /proc/self/exe. int res = readlink("/proc/self/exe", result.path, sizeof(result.path)); @@ -180,4 +325,6 @@ DlInfo GetDlInfo(uintptr_t pc) { return result; } +#endif // __APPLE__ + } // namespace centipede diff --git a/centipede/runner_dl_info.h b/centipede/runner_dl_info.h index d780415f..295f2cb2 100644 --- a/centipede/runner_dl_info.h +++ b/centipede/runner_dl_info.h @@ -27,6 +27,8 @@ namespace centipede { struct DlInfo { uintptr_t start_address; // Address in memory where the object is loaded. uintptr_t size; // Number of bytes in the object. + intptr_t link_offset; // Difference between runtime addresses and link-time + // addresses. char path[4096]; // Pathname from which the object was loaded. void Clear() { memset(this, 0, sizeof(*this)); } diff --git a/centipede/runner_sancov_object.cc b/centipede/runner_sancov_object.cc index abc845b4..4768bb0f 100644 --- a/centipede/runner_sancov_object.cc +++ b/centipede/runner_sancov_object.cc @@ -132,8 +132,8 @@ std::vector SanCovObjectArray::CreatePCTable() const { const auto &object = objects_[i]; for (const auto *ptr = object.pcs_beg; ptr != object.pcs_end; ++ptr) { auto pc_info = *ptr; - // Subtract the ASLR base. - pc_info.pc -= object.dl_info.start_address; + // Convert into the link-time address + pc_info.pc -= object.dl_info.link_offset; result.push_back(pc_info); } } @@ -148,8 +148,8 @@ std::vector SanCovObjectArray::CreateCfTable() const { for (const auto *ptr = object.cfs_beg; ptr != object.cfs_end; ++ptr) { uintptr_t data = *ptr; // CF table is an array of PCs, except for delimiter (Null) and indirect - // call indicator (-1). Subtract the ASLR base only from PCs. - if (data != 0 && data != -1ULL) data -= object.dl_info.start_address; + // call indicator (-1). Convert into link-time address. + if (data != 0 && data != -1ULL) data -= object.dl_info.link_offset; result.push_back(data); } } diff --git a/centipede/runner_utils.cc b/centipede/runner_utils.cc index c1a2b9c2..da87b1aa 100644 --- a/centipede/runner_utils.cc +++ b/centipede/runner_utils.cc @@ -31,6 +31,13 @@ void PrintErrorAndExitIf(bool condition, absl::Nonnull error) { } uintptr_t GetCurrentThreadStackRegionLow() { +#ifdef __APPLE__ + pthread_t self = pthread_self(); + const auto stack_addr = + reinterpret_cast(pthread_get_stackaddr_np(self)); + const auto stack_size = pthread_get_stacksize_np(self); + return stack_addr - stack_size; +#else // __APPLE__ pthread_attr_t attr = {}; if (pthread_getattr_np(pthread_self(), &attr) != 0) { fprintf(stderr, "Failed to get the pthread attr of the current thread.\n"); @@ -48,6 +55,7 @@ uintptr_t GetCurrentThreadStackRegionLow() { RunnerCheck(stack_region_low != 0, "the current thread stack region starts from 0 - unexpected!"); return stack_region_low; +#endif // __APPLE__ } } // namespace centipede diff --git a/centipede/testing/build_defs.bzl b/centipede/testing/build_defs.bzl index 5cbe8fb5..dd2b8d87 100644 --- a/centipede/testing/build_defs.bzl +++ b/centipede/testing/build_defs.bzl @@ -87,6 +87,17 @@ def __sancov_fuzz_target_impl(ctx): # See https://docs.bazel.build/versions/main/skylark/rules.html#runfiles runfiles = ctx.runfiles() + is_macos = ctx.target_platform_has_constraint(ctx.attr._macos_constraint_value[platform_common.ConstraintValueInfo]) + if is_macos: + dsym_dst = ctx.actions.declare_directory(ctx.label.name + ".dSYM", sibling = executable_dst) + ctx.actions.run_shell( + inputs = [executable_dst], + outputs = [dsym_dst], + command = "dsymutil %s -o %s" % (executable_dst.path, dsym_dst.path), + execution_requirements = {"no-remote-exec": ""}, + ) + runfiles = runfiles.merge(ctx.runfiles(files = [dsym_dst])) + # The transition transforms scalar attributes into lists, # so we need to index into the list first. fuzz_target = ctx.attr.fuzz_target[0] @@ -108,6 +119,9 @@ __sancov_fuzz_target = rule( "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), + "_macos_constraint_value": attr.label( + default = "@platforms//os:macos", + ), }, executable = True, ) @@ -151,9 +165,15 @@ def centipede_fuzz_target( copts = copts, linkopts = linkopts + [ "-ldl", - "-lrt", - "-lpthread" - ], + "-lpthread", + ] + select({ + "@platforms//os:macos": [], + "//conditions:default": [ + "-lrt", # for shm_open + ], + }), + # For dsymutils to locate files properly. + tags = ["no-sandbox"], testonly = True, ) diff --git a/centipede/testing/centipede_main_test.sh b/centipede/testing/centipede_main_test.sh index 9c36ee7e..80478a75 100755 --- a/centipede/testing/centipede_main_test.sh +++ b/centipede/testing/centipede_main_test.sh @@ -71,7 +71,7 @@ test_debug_symbols() { --symbolizer_path="${LLVM_SYMBOLIZER}" | tee "${LOG}" centipede::assert_regex_in_file 'Custom mutator detected: will use it' "${LOG}" # Note: the test assumes LLVMFuzzerTestOneInput is defined on a specific line. - centipede::assert_regex_in_file "FUNC: LLVMFuzzerTestOneInput .*testing/test_fuzz_target.cc:70" "${LOG}" + centipede::assert_regex_in_file "FUNC: LLVMFuzzerTestOneInput .*testing/test_fuzz_target.cc:71" "${LOG}" centipede::assert_regex_in_file "EDGE: LLVMFuzzerTestOneInput .*testing/test_fuzz_target.cc" "${LOG}" echo "============ ${FUNC}: add func1/func2-A inputs to the corpus." diff --git a/centipede/testing/instrumentation_test.sh b/centipede/testing/instrumentation_test.sh index 8d9ef522..ba2f1a43 100755 --- a/centipede/testing/instrumentation_test.sh +++ b/centipede/testing/instrumentation_test.sh @@ -37,7 +37,11 @@ CENTIPEDE_RUNNER_FLAGS=":dump_binary_info:arg1=${pc_table}:arg2=${unused1}:arg3= "${target}" # Check the pc table size. -size=$(stat -c %s "${pc_table}") +if [[ "$OSTYPE" == 'darwin'* ]]; then + size=$(stat -f %z "${pc_table}") +else + size=$(stat -c %s "${pc_table}") +fi echo "pc table size: ${size}" (( size < 1 )) && die "pc table is too small: ${size}" (( size > ALLOWED_SIZE )) && die "pc table is too large: ${size}" diff --git a/centipede/testing/runner_test.sh b/centipede/testing/runner_test.sh index 9073d639..3f7c6936 100755 --- a/centipede/testing/runner_test.sh +++ b/centipede/testing/runner_test.sh @@ -85,14 +85,19 @@ echo ======== Run f1 func1 "${target}" "${f1}" "${func1}" check_features_files_f1_and_f02 -# Check OOM. The test target allocates 4Gb, and we make sure -# this can be detected with appropriate limit (address_space_limit_mb), -# and can not be detected with a larger limit. -echo ======== Check OOM behaviour with address_space_limit_mb -CENTIPEDE_RUNNER_FLAGS=":address_space_limit_mb=4096:" $target "${oom}" \ - && die "failed to die on 4G OOM (address_space_limit_mb)" -# must pass. -CENTIPEDE_RUNNER_FLAGS=":address_space_limit_mb=8192:" $target "${oom}" + +# Address space limit is ignored by MacOS. +# Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=853873#c2 +if [[ "${OSTYPE}" != 'darwin'* ]]; then + # Check OOM. The test target allocates 4Gb, and we make sure + # this can be detected with appropriate limit (address_space_limit_mb), + # and can not be detected with a larger limit. + echo ======== Check OOM behaviour with address_space_limit_mb + CENTIPEDE_RUNNER_FLAGS=":address_space_limit_mb=4096:" $target "${oom}" \ + && die "failed to die on 4G OOM (address_space_limit_mb)" + # must pass. + CENTIPEDE_RUNNER_FLAGS=":address_space_limit_mb=8192:" $target "${oom}" +fi echo ======== Check OOM behaviour with rss_limit_mb CENTIPEDE_RUNNER_FLAGS=":rss_limit_mb=4096:" $target "${oom}" \ diff --git a/centipede/testing/test_fuzz_target.cc b/centipede/testing/test_fuzz_target.cc index e9ca85eb..32e07ebd 100644 --- a/centipede/testing/test_fuzz_target.cc +++ b/centipede/testing/test_fuzz_target.cc @@ -13,6 +13,7 @@ // limitations under the License. // A fuzz target used for testing Centipede. +#include #include #include #include @@ -153,9 +154,15 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { } // Disable and free the previous signal stack. stack_t disabled_sigstk = {}; +#ifdef __APPLE__ + // Needed for MacOS. + // Reference: + // https://chromium.googlesource.com/native_client/src/native_client/+/ad617ab7dd5f23a67fcff244b3c3263ffcc7e66d/src/trusted/service_runtime/posix/nacl_signal_stack.c#117 + disabled_sigstk.ss_size = MINSIGSTKSZ; +#endif disabled_sigstk.ss_flags = SS_DISABLE; if (sigaltstack(&disabled_sigstk, nullptr) != 0) { - printf("failed to disable the signal stack\n"); + printf("failed to disable the signal stack: %d\n", errno); abort(); } free(sigstk.ss_sp);