diff --git a/.github/workflows/test_workflow.yml b/.github/workflows/test_workflow.yml index 27c052591..dda78c7f1 100644 --- a/.github/workflows/test_workflow.yml +++ b/.github/workflows/test_workflow.yml @@ -191,6 +191,13 @@ jobs: name: signal-safety-violation-glibc-${{ matrix.java_version }}-${{ matrix.config }}-amd64 path: /tmp/signal-safety-violation.txt if-no-files-found: ignore + - name: Upload ASan logs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && matrix.config == 'asan' + with: + name: asan-logs-glibc-${{ matrix.java_version }}-amd64 + path: /tmp/asan.log.* + if-no-files-found: ignore test-linux-musl-amd64: needs: [cache-jdks, filter-musl-configs] @@ -317,6 +324,13 @@ jobs: name: signal-safety-violation-musl-${{ matrix.java_version }}-${{ matrix.config }}-amd64 path: /tmp/signal-safety-violation.txt if-no-files-found: ignore + - name: Upload ASan logs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && matrix.config == 'asan' + with: + name: asan-logs-musl-${{ matrix.java_version }}-amd64 + path: /tmp/asan.log.* + if-no-files-found: ignore test-linux-glibc-aarch64: needs: cache-jdks @@ -495,6 +509,13 @@ jobs: name: signal-safety-violation-glibc-${{ matrix.java_version }}-${{ matrix.config }}-aarch64 path: /tmp/signal-safety-violation.txt if-no-files-found: ignore + - name: Upload ASan logs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && matrix.config == 'asan' + with: + name: asan-logs-glibc-${{ matrix.java_version }}-aarch64 + path: /tmp/asan.log.* + if-no-files-found: ignore test-linux-musl-aarch64: needs: [cache-jdks, filter-musl-configs] @@ -599,3 +620,10 @@ jobs: name: signal-safety-violation-musl-${{ matrix.java_version }}-${{ matrix.config }}-aarch64 path: /tmp/signal-safety-violation.txt if-no-files-found: ignore + - name: Upload ASan logs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: failure() && matrix.config == 'asan' + with: + name: asan-logs-musl-${{ matrix.java_version }}-aarch64 + path: /tmp/asan.log.* + if-no-files-found: ignore diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt index b2be2b586..fab7d91d6 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt @@ -211,10 +211,22 @@ object ConfigurationPresets { if (libasan != null) { config.testEnvironment.apply { put("LD_PRELOAD", libasan) - put("ASAN_OPTIONS", "allocator_may_return_null=1:unwind_abort_on_malloc=1:use_sigaltstack=0:detect_stack_use_after_return=0:handle_segv=0:halt_on_error=0:abort_on_error=0:print_stacktrace=1:symbolize=1:suppressions=$rootDir/gradle/sanitizers/asan.supp") + put("ASAN_OPTIONS", "allocator_may_return_null=1:unwind_abort_on_malloc=1:use_sigaltstack=0:detect_stack_use_after_return=0:handle_segv=0:halt_on_error=0:abort_on_error=0:print_stacktrace=1:symbolize=1:log_path=/tmp/asan.log:suppressions=$rootDir/gradle/sanitizers/asan.supp") put("UBSAN_OPTIONS", "halt_on_error=0:abort_on_error=0:print_stacktrace=1:suppressions=$rootDir/gradle/sanitizers/ubsan.supp") put("LSAN_OPTIONS", "detect_leaks=0") } + // JDK 25's G1GC reserves heap virtual address space starting just below 2 GB + // (0x7fff7000), which is exactly where ASan needs to mmap its shadow bytes + // [0x7fff7000-0x10007fff7fff]. Force the heap to a very low base address so + // the entire JVM footprint (heap + class space + code cache) stays well below + // the shadow range and the two regions no longer conflict. + if (PlatformUtils.testJvmMajorVersion() >= 25) { + config.testJvmArgs.addAll(listOf( + "-XX:HeapBaseMinAddress=0x4000000", + "-Xmx512m", + "-XX:CompressedClassSpaceSize=256m" + )) + } } } Platform.MACOS -> { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/model/BuildConfiguration.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/model/BuildConfiguration.kt index aa7a5c3c7..1224abd67 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/model/BuildConfiguration.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/model/BuildConfiguration.kt @@ -15,6 +15,7 @@ abstract class BuildConfiguration @Inject constructor( abstract val compilerArgs: ListProperty abstract val linkerArgs: ListProperty abstract val testEnvironment: MapProperty + abstract val testJvmArgs: ListProperty abstract val active: Property override fun getName(): String = configName @@ -23,6 +24,7 @@ abstract class BuildConfiguration @Inject constructor( // Default to active unless overridden active.convention(true) testEnvironment.convention(emptyMap()) + testJvmArgs.convention(emptyList()) } fun isActiveFor(targetPlatform: Platform, targetArch: Architecture): Boolean { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt index c6fb32129..b8e5249cb 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt @@ -367,4 +367,24 @@ object PlatformUtils { false } } + + /** + * Returns the major version of the test JVM (e.g. 8, 11, 17, 21, 25). + * Returns 0 if the version cannot be determined. + */ + fun testJvmMajorVersion(): Int { + val javaHome = testJavaHome() + return try { + val process = ProcessBuilder("$javaHome/bin/java", "-version") + .redirectErrorStream(true) + .start() + val output = process.inputStream.bufferedReader().readText() + process.waitFor(10, TimeUnit.SECONDS) + // version line: java version "1.8.0_xxx" or java version "21.0.x" etc. + val match = Regex("""version "(?:1\.)?(\d+)""").find(output) + match?.groupValues?.get(1)?.toIntOrNull() ?: 0 + } catch (_: Exception) { + 0 + } + } } diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt index 56827cb28..4dfa580d7 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt @@ -111,6 +111,7 @@ class ProfilerTestPlugin : Plugin { val testClasspath: FileCollection, val standardJvmArgs: List, val extraJvmArgs: List, + val configJvmArgs: List, val systemProperties: Map, val environmentVariables: Map ) @@ -164,6 +165,7 @@ class ProfilerTestPlugin : Plugin { testClasspath = testClasspath, standardJvmArgs = extension.standardJvmArgs.get(), extraJvmArgs = extension.extraJvmArgs.get(), + configJvmArgs = config.testJvmArgs.get(), systemProperties = systemProps, environmentVariables = envVars ) @@ -247,6 +249,7 @@ class ProfilerTestPlugin : Plugin { } allArgs.addAll(testConfig.extraJvmArgs) + allArgs.addAll(testConfig.configJvmArgs) testTask.jvmArgs(allArgs) } @@ -303,6 +306,7 @@ class ProfilerTestPlugin : Plugin { allArgs.add("-Djava.library.path=${extension.nativeLibDir.get().asFile.absolutePath}") } allArgs.addAll(testConfig.extraJvmArgs) + allArgs.addAll(testConfig.configJvmArgs) // System properties testConfig.systemProperties.forEach { (key, value) -> diff --git a/ddprof-lib/src/main/cpp/buffers.h b/ddprof-lib/src/main/cpp/buffers.h index 098752258..700a802bc 100644 --- a/ddprof-lib/src/main/cpp/buffers.h +++ b/ddprof-lib/src/main/cpp/buffers.h @@ -80,6 +80,7 @@ class Buffer { _data[_offset++] = v; } + __attribute__((no_sanitize("undefined"))) __attribute__((no_sanitize("bounds"))) void put16(short v) { assert(_offset + 2 < limit()); diff --git a/ddprof-lib/src/main/cpp/callTraceStorage.h b/ddprof-lib/src/main/cpp/callTraceStorage.h index 4735e18f2..7affe9ca3 100644 --- a/ddprof-lib/src/main/cpp/callTraceStorage.h +++ b/ddprof-lib/src/main/cpp/callTraceStorage.h @@ -34,7 +34,7 @@ class CallTraceStorage { // Reserved trace ID for dropped samples due to contention // Real trace IDs are generated as (instance_id << 32) | slot, where instance_id starts from 1 // Any ID with 0 in upper 32 bits is guaranteed to not clash with real trace IDs - static const u64 DROPPED_TRACE_ID = 1ULL; + static constexpr u64 DROPPED_TRACE_ID = 1ULL; // Static dropped trace object that appears in JFR constant pool static CallTrace* getDroppedTrace();