Skip to content

Conversation

TheShiftedBit
Copy link
Contributor

@TheShiftedBit TheShiftedBit commented Sep 18, 2025

The following assertion was being triggered:

assert.h assertion failed at llvm-project/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp:237 in void fuzzer::TracePC::UpdateObservedPCs(): M.Size() == (size_t)(ModulePCTable[i].Stop - ModulePCTable[i].Start)

The bug

When built with -fsanitize=fuzzer, each “module” (.so file, or the binary itself) will be instrumented, and when loaded into the process will make a call to these two functions:

  • __sanitizer_cov_8bit_counters_init
  • __sanitizer_cov_pcs_init

Each of these is called with start and end pointers defining an array.

In libFuzzer, these functions are implemented with HandleInline8bitCountersInit and HandlePCsInit. Each of them pushes back the provided pointers into a separate array, Modules and ModulePCTable respectively. These arrays are meant to be kept in-sync; index i into Modules should refer to the same .so as index i into ModulePCTable. The assertion was triggering because these lists got out-of-sync.

The problem is that the 8bit handler contains this line:

if (Start == Stop) return;

but the PC handler contains no such corresponding line. This meant that if a module was ever instrumented but “empty” (its 8bit counter and PC arrays were both of length 0), then its PC array would still be added but its 8bit counter array would not.

Why this issue was never seen before

The circumstances to trigger this issue are unusual:

  • You need a compilation unit that doesn't contain any code (though it may contain global variable declarations and similar). That doesn't happen very often.
  • That compilation unit must be dynamically linked, not statically linked. If statically linked, it’ll be merged into a single “module” with the main binary, and the arrays will be merged as well; you won’t end up with length-0 arrays.
  • To notice the issue, assertions must be enabled. If disabled, libFuzzer will be buggy (it may have worse coverage), but it won't crash, and "worse coverage" is extremely unlikely to be noticed.

This change

This change solves the issue by adding the same if (Start == Stop) return; check to HandlePCsInit. This prevents the arrays from getting out-of-sync. This change also adds a test that identifies the previous issue when compiled with assertions enabled, but now passes with the fix.

The following assertion was being triggered:
```
assert.h assertion failed at third_party/llvm/llvm-project/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp:237 in void fuzzer::TracePC::UpdateObservedPCs(): M.Size() == (size_t)(ModulePCTable[i].Stop - ModulePCTable[i].Start)
```

When built with `-fsanitize=fuzzer`, each “module” (.so file, or the binary itself) will be instrumented, and when loaded into the process will make a call to these two functions:
`__sanitizer_cov_8bit_counters_init`
`__sanitizer_cov_pcs_init`
Each of these is called with start and end pointers defining an array.

In libFuzzer, these functions are implemented with `HandleInline8bitCountersInit` and `HandlePCsInit`. Each of them pushes back the provided pointers into a separate array, `Modules` and `ModulePCTable` respectively. These arrays are meant to be kept in-sync; index i into Modules should refer to the same `.so` as index i into ModulePCTable. The assertion was triggering because these lists got out-of-sync.

The problem is that the 8bit handler contains this line:
```
if (Start == Stop) return;
```
but the PC handler contains no such corresponding line. This meant that if a module was ever instrumented but “empty” (its 8bit counter and PC arrays were both of length 0), then its PC array would still be added but its 8bit counter array would not.

This issue was likely never spotted before because the circumstances to
trigger it are unusual:
 - You need a compilation unit that doesn't contain any code (though it may contain global variable declarations and similar). That doesn't happen very often.
 - That compilation unit must be dynamically linked, not statically linked. If statically linked, it’ll be merged into a single “module” with the main binary, and the arrays will be merged as well; you won’t end up with length-0 arrays.
 - To notice the issue, assertions must be enabled. If disabled,
   libFuzzer will be buggy (it may have worse coverage), but it won't crash, and "worse coverage" is extremely unlikely to be noticed.

This change solves the issue by adding the same `if (Start == Stop) return;` check to `HandlePCsInit`. This prevents the arrays from getting out-of-sync. This change also adds a test that identifies the previous issue when compiled with assertions enabled, but now passes with the fix.
Copy link

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Sep 18, 2025

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: Bitshift (TheShiftedBit)

Changes

The following assertion was being triggered:

assert.h assertion failed at llvm-project/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp:237 in void fuzzer::TracePC::UpdateObservedPCs(): M.Size() == (size_t)(ModulePCTable[i].Stop - ModulePCTable[i].Start)

When built with -fsanitize=fuzzer, each “module” (.so file, or the binary itself) will be instrumented, and when loaded into the process will make a call to these two functions: __sanitizer_cov_8bit_counters_init
__sanitizer_cov_pcs_init
Each of these is called with start and end pointers defining an array.

In libFuzzer, these functions are implemented with HandleInline8bitCountersInit and HandlePCsInit. Each of them pushes back the provided pointers into a separate array, Modules and ModulePCTable respectively. These arrays are meant to be kept in-sync; index i into Modules should refer to the same .so as index i into ModulePCTable. The assertion was triggering because these lists got out-of-sync.

The problem is that the 8bit handler contains this line:

if (Start == Stop) return;

but the PC handler contains no such corresponding line. This meant that if a module was ever instrumented but “empty” (its 8bit counter and PC arrays were both of length 0), then its PC array would still be added but its 8bit counter array would not.

This issue was likely never spotted before because the circumstances to trigger it are unusual:

  • You need a compilation unit that doesn't contain any code (though it may contain global variable declarations and similar). That doesn't happen very often.
  • That compilation unit must be dynamically linked, not statically linked. If statically linked, it’ll be merged into a single “module” with the main binary, and the arrays will be merged as well; you won’t end up with length-0 arrays.
  • To notice the issue, assertions must be enabled. If disabled, libFuzzer will be buggy (it may have worse coverage), but it won't crash, and "worse coverage" is extremely unlikely to be noticed.

This change solves the issue by adding the same if (Start == Stop) return; check to HandlePCsInit. This prevents the arrays from getting out-of-sync. This change also adds a test that identifies the previous issue when compiled with assertions enabled, but now passes with the fix.


Full diff: https://github.com/llvm/llvm-project/pull/159661.diff

3 Files Affected:

  • (modified) compiler-rt/lib/fuzzer/FuzzerTracePC.cpp (+3)
  • (added) compiler-rt/test/fuzzer/SimulateEmptyModuleTest.cpp (+60)
  • (added) compiler-rt/test/fuzzer/empty-module.test (+7)
diff --git a/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp b/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
index 7f4e8ef91c447..7bd1a0870c593 100644
--- a/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
+++ b/compiler-rt/lib/fuzzer/FuzzerTracePC.cpp
@@ -69,6 +69,9 @@ void TracePC::HandleInline8bitCountersInit(uint8_t *Start, uint8_t *Stop) {
 }
 
 void TracePC::HandlePCsInit(const uintptr_t *Start, const uintptr_t *Stop) {
+  if (Start == Stop) {
+    return;
+  }
   const PCTableEntry *B = reinterpret_cast<const PCTableEntry *>(Start);
   const PCTableEntry *E = reinterpret_cast<const PCTableEntry *>(Stop);
   if (NumPCTables && ModulePCTable[NumPCTables - 1].Start == B) return;
diff --git a/compiler-rt/test/fuzzer/SimulateEmptyModuleTest.cpp b/compiler-rt/test/fuzzer/SimulateEmptyModuleTest.cpp
new file mode 100644
index 0000000000000..de20e1e03570d
--- /dev/null
+++ b/compiler-rt/test/fuzzer/SimulateEmptyModuleTest.cpp
@@ -0,0 +1,60 @@
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// Like SimpleTest, but simulates an "empty" module (i.e. one without any functions to instrument).
+// This reproduces a previous bug (when libFuzzer is compiled with assertions enabled).
+
+#include <assert.h>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <iostream>
+#include <ostream>
+
+extern "C" {
+void __sanitizer_cov_8bit_counters_init(uint8_t *Start, uint8_t *Stop);
+void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
+                              const uintptr_t *pcs_end);
+}
+
+void dummy_func() {}
+
+uint8_t empty_8bit_counters[0];
+uintptr_t empty_pcs[0];
+
+uint8_t fake_8bit_counters[1] = {0};
+uintptr_t fake_pcs[2] = {reinterpret_cast<uintptr_t>(&dummy_func),
+                         reinterpret_cast<uintptr_t>(&dummy_func)};
+
+// Register two modules at program launch (same time they'd normally be registered).
+// Triggering the bug requires loading an empty module, then a non-empty module after it.
+bool dummy = []() {
+  // First, simulate loading an empty module.
+  __sanitizer_cov_8bit_counters_init(empty_8bit_counters, empty_8bit_counters);
+  __sanitizer_cov_pcs_init(empty_pcs, empty_pcs);
+
+  // Next, simulate loading a non-empty module.
+  __sanitizer_cov_8bit_counters_init(fake_8bit_counters,
+                                     fake_8bit_counters + 1);
+  __sanitizer_cov_pcs_init(fake_pcs, fake_pcs + 2);
+
+  return true;
+}();
+
+static volatile int Sink;
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
+  assert(Data);
+  if (Size > 0 && Data[0] == 'H') {
+    Sink = 1;
+    if (Size > 1 && Data[1] == 'i') {
+      Sink = 2;
+      if (Size > 2 && Data[2] == '!') {
+        std::cout << "BINGO; Found the target, exiting\n" << std::flush;
+        exit(0);
+      }
+    }
+  }
+  return 0;
+}
diff --git a/compiler-rt/test/fuzzer/empty-module.test b/compiler-rt/test/fuzzer/empty-module.test
new file mode 100644
index 0000000000000..2f5c882168678
--- /dev/null
+++ b/compiler-rt/test/fuzzer/empty-module.test
@@ -0,0 +1,7 @@
+CHECK: BINGO
+RUN: %cpp_compiler %S/SimulateEmptyModuleTest.cpp -o %t-SimulateEmptyModuleTest
+
+RUN: not %run %t-SimulateEmptyModuleTest         2>&1 | FileCheck %s
+
+# only_ascii mode. Will perform some minimal self-validation.
+RUN: not %run %t-SimulateEmptyModuleTest -only_ascii=1 2>&1

@vitalybuka vitalybuka self-requested a review September 18, 2025 22:26
@vitalybuka vitalybuka enabled auto-merge (squash) September 18, 2025 23:59
@vitalybuka vitalybuka merged commit 3f52e97 into llvm:main Sep 19, 2025
13 checks passed
Copy link

@TheShiftedBit Congratulations on having your first Pull Request (PR) merged into the LLVM Project!

Your changes will be combined with recent changes from other authors, then tested by our build bots. If there is a problem with a build, you may receive a report in an email or a comment on this PR.

Please check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues.

How to do this, and the rest of the post-merge process, is covered in detail here.

If your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again.

If you don't get any reports, no action is required from you. Your changes are working as expected, well done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants