Skip to content

Commit 9d0deb0

Browse files
committed
test: verify PC symbolization across all layers
Kotlin unit test: instruments CoverageInstrumentationTarget and checks that buildEdgeLocations() produces correct source file path, method names, function-entry flags, and non-zero line numbers. C++ unit test: exercises SymbolizePC format specifier parsing (%p, %F, %L, %s, %l, %c) and verifies buffer truncation safety. Shell integration test: runs the native launcher with -print_pcs=1 on a trivial target and greps for symbolized NEW_PC lines.
1 parent c000f66 commit 9d0deb0

7 files changed

Lines changed: 286 additions & 0 deletions

File tree

src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,14 @@ cc_test(
189189
"@rules_jni//jni",
190190
],
191191
)
192+
193+
cc_test(
194+
name = "synthetic_symbolizer_test",
195+
size = "small",
196+
srcs = ["synthetic_symbolizer_test.cpp"],
197+
deps = [
198+
":synthetic_symbolizer",
199+
"@googletest//:gtest",
200+
"@googletest//:gtest_main",
201+
],
202+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2026 Code Intelligence GmbH
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "synthetic_symbolizer.h"
16+
17+
#include <cstdint>
18+
#include <cstring>
19+
#include <string>
20+
21+
#include "gtest/gtest.h"
22+
23+
// Stubs for libFuzzer symbols pulled in transitively via counters_tracker.
24+
extern "C" {
25+
void __sanitizer_cov_8bit_counters_init(uint8_t *, uint8_t *) {}
26+
void __sanitizer_cov_pcs_init(const uintptr_t *, const uintptr_t *) {}
27+
size_t __sanitizer_cov_get_observed_pcs(uintptr_t **) { return 0; }
28+
}
29+
30+
// Helper: call SymbolizePC with an unregistered PC and return the result.
31+
static std::string Symbolize(uintptr_t pc, const char *fmt,
32+
size_t buf_size = 1024) {
33+
std::string buf(buf_size, '\0');
34+
jazzer::SymbolizePC(pc, fmt, buf.data(), buf_size);
35+
return {buf.c_str()};
36+
}
37+
38+
// The default libFuzzer format for DescribePC is "%p %F %L".
39+
// With no registered locations, we should get clean <unknown> fallback.
40+
TEST(SyntheticSymbolizerTest, UnregisteredPCProducesUnknownFallback) {
41+
auto result = Symbolize(42, "%p %F %L");
42+
// %p should be eaten (virtual PCs are meaningless), leaving "%F %L".
43+
EXPECT_NE(std::string::npos, result.find("in <unknown>"));
44+
EXPECT_NE(std::string::npos, result.find("<unknown>:0"));
45+
// No hex address should appear (the %p was consumed).
46+
EXPECT_EQ(std::string::npos, result.find("0x"));
47+
}
48+
49+
// A small buffer should truncate without crashing.
50+
TEST(SyntheticSymbolizerTest, SmallBufferTruncatesSafely) {
51+
char tiny[8] = {};
52+
jazzer::SymbolizePC(42, "%F %L", tiny, sizeof(tiny));
53+
// Must be null-terminated and not overflow.
54+
EXPECT_LT(strlen(tiny), sizeof(tiny));
55+
56+
// Zero-size buffer is a no-op.
57+
char zero = 'X';
58+
jazzer::SymbolizePC(42, "%F", &zero, 0);
59+
EXPECT_EQ('X', zero);
60+
}
61+
62+
// Verify individual format specifiers produce the right fallback shape.
63+
TEST(SyntheticSymbolizerTest, FormatSpecifiers) {
64+
EXPECT_EQ("in <unknown>", Symbolize(42, "%F"));
65+
EXPECT_EQ("<unknown>:0", Symbolize(42, "%L"));
66+
EXPECT_EQ("<unknown>", Symbolize(42, "%s"));
67+
EXPECT_EQ("0", Symbolize(42, "%l"));
68+
EXPECT_EQ("0", Symbolize(42, "%c"));
69+
// Literal text passes through.
70+
EXPECT_EQ("hello", Symbolize(42, "hello"));
71+
}

src/test/java/com/code_intelligence/jazzer/instrumentor/BUILD.bazel

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@ wrapped_kt_jvm_test(
5050
],
5151
)
5252

53+
wrapped_kt_jvm_test(
54+
name = "edge_location_capture_test",
55+
size = "small",
56+
srcs = [
57+
"CoverageInstrumentationTarget.java",
58+
"EdgeLocationCaptureTest.kt",
59+
"MockCoverageMap.java",
60+
],
61+
associates = [
62+
"//src/main/java/com/code_intelligence/jazzer/instrumentor:instrumentor",
63+
],
64+
test_class = "com.code_intelligence.jazzer.instrumentor.EdgeLocationCaptureTest",
65+
deps = [
66+
":patch_test_utils",
67+
"//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map",
68+
"@maven//:junit_junit",
69+
"@rules_kotlin//kotlin/compiler:kotlin-test",
70+
],
71+
)
72+
5373
wrapped_kt_jvm_test(
5474
name = "descriptor_utils_test",
5575
size = "small",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
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+
* http://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+
17+
package com.code_intelligence.jazzer.instrumentor
18+
19+
import com.code_intelligence.jazzer.instrumentor.PatchTestUtils.classToBytecode
20+
import org.junit.Test
21+
import kotlin.test.assertEquals
22+
import kotlin.test.assertNotNull
23+
import kotlin.test.assertTrue
24+
25+
class EdgeLocationCaptureTest {
26+
@Test
27+
fun testEdgeLocationCapture() {
28+
val internalClassName = CoverageInstrumentationTarget::class.java.name.replace('.', '/')
29+
val instrumentor =
30+
EdgeCoverageInstrumentor(
31+
ClassInstrumentor.defaultEdgeCoverageStrategy,
32+
MockCoverageMap::class.java,
33+
0,
34+
)
35+
instrumentor.instrument(internalClassName, classToBytecode(CoverageInstrumentationTarget::class.java))
36+
37+
val locations = instrumentor.buildEdgeLocations()
38+
assertNotNull(locations, "Expected non-null locations for a class with edges")
39+
40+
// Source file should combine the package prefix with the SourceFile attribute.
41+
assertEquals(
42+
"com/code_intelligence/jazzer/instrumentor/CoverageInstrumentationTarget.java",
43+
locations.sourceFile,
44+
)
45+
46+
// Method names should be qualified as SimpleClassName.method.
47+
assertTrue(
48+
locations.methodNames.any { it == "CoverageInstrumentationTarget.<init>" },
49+
"Expected constructor in method names, got: ${locations.methodNames.toList()}",
50+
)
51+
assertTrue(
52+
locations.methodNames.any { it == "CoverageInstrumentationTarget.selfCheck" },
53+
"Expected selfCheck in method names, got: ${locations.methodNames.toList()}",
54+
)
55+
56+
// Flat array must have exactly 2 ints per edge (packedLine, methodIdx).
57+
assertEquals(instrumentor.numEdges * 2, locations.edgeData.size)
58+
59+
// First edge should have the function-entry bit set (sign bit).
60+
val firstPackedLine = locations.edgeData[0]
61+
assertTrue(firstPackedLine < 0, "First edge should have function-entry bit (sign bit) set")
62+
63+
// Its actual line number should be positive (class was compiled with debug info).
64+
val firstLine = firstPackedLine and 0x7FFFFFFF
65+
assertTrue(firstLine > 0, "Line number should be > 0 for a class with debug info")
66+
67+
// Second edge of the same method should NOT have the function-entry bit set.
68+
// Find the second edge that shares the same methodIdx as the first.
69+
val firstMethodIdx = locations.edgeData[1]
70+
for (i in 1 until instrumentor.numEdges) {
71+
if (locations.edgeData[2 * i + 1] == firstMethodIdx) {
72+
val subsequentPackedLine = locations.edgeData[2 * i]
73+
assertTrue(subsequentPackedLine >= 0, "Subsequent edge should not have function-entry bit")
74+
break
75+
}
76+
}
77+
}
78+
}

tests/BUILD.bazel

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,30 @@ java_binary(
451451
srcs = ["src/test/java/com/example/CrashResistantCoverageTarget.java"],
452452
)
453453

454+
java_binary(
455+
name = "PrintPcsTarget",
456+
testonly = True,
457+
srcs = ["src/test/java/com/example/PrintPcsTarget.java"],
458+
create_executable = False,
459+
deps = ["//deploy:jazzer-api"],
460+
)
461+
462+
sh_test(
463+
name = "print_pcs_symbolization_test",
464+
size = "large",
465+
srcs = ["src/test/shell/print_pcs_symbolization_test.sh"],
466+
args = [
467+
"$(rlocationpath //launcher:jazzer)",
468+
"$(rlocationpath :PrintPcsTarget_deploy.jar)",
469+
],
470+
data = [
471+
":PrintPcsTarget_deploy.jar",
472+
"//launcher:jazzer",
473+
],
474+
target_compatible_with = LINUX_ONLY,
475+
deps = ["@bazel_tools//tools/bash/runfiles"],
476+
)
477+
454478
sh_test(
455479
name = "crash_resistant_coverage_test",
456480
srcs = ["src/test/shell/crash_resistant_coverage_test.sh"],
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2026 Code Intelligence GmbH
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+
* http://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+
17+
package com.example;
18+
19+
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
20+
21+
/** Minimal fuzz target used to verify -print_pcs symbolization. */
22+
public class PrintPcsTarget {
23+
public static void fuzzerTestOneInput(FuzzedDataProvider data) {
24+
int x = data.consumeInt();
25+
if (x > 1000) {
26+
sink(x);
27+
}
28+
}
29+
30+
private static void sink(int x) {}
31+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/bin/bash
2+
#
3+
# Copyright 2026 Code Intelligence GmbH
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
# Verify that -print_pcs=1 produces symbolized Java source locations
19+
# instead of meaningless hex addresses.
20+
21+
# --- begin runfiles.bash initialization v2 ---
22+
set -uo pipefail; f=bazel_tools/tools/bash/runfiles/runfiles.bash
23+
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
24+
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
25+
source "$0.runfiles/$f" 2>/dev/null || \
26+
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
27+
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
28+
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
29+
# --- end runfiles.bash initialization v2 ---
30+
31+
function fail() {
32+
echo "FAILED: $1"
33+
exit 1
34+
}
35+
36+
JAZZER="$(rlocation "$1")"
37+
TARGET_JAR="$(rlocation "$2")"
38+
output="$TEST_TMPDIR/output"
39+
40+
# A short burst of runs is enough to discover the target's coverage edges.
41+
"$JAZZER" --cp="$TARGET_JAR" --target_class=com.example.PrintPcsTarget \
42+
--instrumentation_includes="com.example.**" \
43+
-print_pcs=1 -runs=10 2>&1 | tee "$output" || true
44+
45+
# Verify at least one NEW_PC line has a symbolized Java source location:
46+
# "in <ClassName>.<method> <path>.java:<line>"
47+
if ! grep -qP 'NEW_PC:.*in \S+\.\S+ \S+\.java:\d+' "$output"; then
48+
echo "Output was:"
49+
cat "$output"
50+
fail "Expected symbolized NEW_PC lines with Java source locations"
51+
fi

0 commit comments

Comments
 (0)