Skip to content

Commit 8517bb9

Browse files
committed
Rework chaos_check.sh into a standalone runner; default glibc heap-corruption detection
utils/run-chaos-harness.sh is the new standalone entry point (usable from CI or by hand); chaos_check.sh becomes a thin wrapper around it. Also defaults MALLOC_CHECK_/MALLOC_PERTURB_ for glibc-backed test runs (this script and the shared ddprof-test Gradle test-task configuration), turning silent heap corruption into an immediate, attributable SIGABRT instead of a crash much later in an unrelated allocation. Skipped on musl and whenever a sanitizer/allocator already replaces malloc via LD_PRELOAD. The chosen MALLOC_PERTURB_ byte is logged so a corruption abort can be reproduced.
1 parent 163ee95 commit 8517bb9

3 files changed

Lines changed: 333 additions & 172 deletions

File tree

.gitlab/reliability/chaos_check.sh

Lines changed: 11 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,17 @@
11
#!/usr/bin/env bash
2-
3-
set +e # Disable exit on error
2+
#
3+
# CI entry point for the chaos harness. Thin wrapper around
4+
# utils/run-chaos-harness.sh — this file only supplies the CI-specific
5+
# caching paths (so repeated scheduled runs on the same runner reuse the
6+
# downloaded JDK/agent jar) and the fixed hs_err.log location the pipeline's
7+
# `artifacts:` block expects at the repo root. All the actual build/run logic
8+
# lives in the standalone script, which is also runnable by hand.
49

510
HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
611
ROOT="$( cd "${HERE}/../.." >/dev/null 2>&1 && pwd )"
712

8-
RUNTIME=${1}
9-
CONFIG=${2:-profiler+tracer}
10-
ALLOCATOR=${3:-gmalloc}
11-
12-
echo "Chaos run: runtime=${RUNTIME}s config=${CONFIG} allocator=${ALLOCATOR}"
13-
14-
CHAOS_JDK="${CHAOS_JDK:-21.0.3-tem}"
15-
# CHAOS_JDK uses sdkman notation (<version>-<dist>); extract major for Adoptium API.
16-
JDK_MAJOR="${CHAOS_JDK%%.*}"
17-
JDK_ARCH=$(uname -m | sed 's/x86_64/x64/')
18-
JDK_INSTALL_DIR="/opt/jdk-${CHAOS_JDK}"
19-
20-
if [ ! -x "${JDK_INSTALL_DIR}/bin/java" ]; then
21-
TMP=$(mktemp -d)
22-
DL_URL="https://api.adoptium.net/v3/binary/latest/${JDK_MAJOR}/ga/linux/${JDK_ARCH}/jdk/hotspot/normal/eclipse"
23-
echo "Downloading JDK ${CHAOS_JDK} (major ${JDK_MAJOR}) from Adoptium..."
24-
if ! curl -fsSL --max-time 300 "${DL_URL}" -o "${TMP}/jdk.tar.gz"; then
25-
echo "FAIL:JDK ${CHAOS_JDK} download failed" >&2
26-
rm -rf "${TMP}"
27-
exit 1
28-
fi
29-
mkdir -p "${JDK_INSTALL_DIR}"
30-
tar -xzf "${TMP}/jdk.tar.gz" -C "${JDK_INSTALL_DIR}" --strip-components=1
31-
rm -rf "${TMP}"
32-
fi
33-
34-
if [ ! -x "${JDK_INSTALL_DIR}/bin/java" ]; then
35-
echo "FAIL:JDK ${CHAOS_JDK} not available after install" >&2
36-
exit 1
37-
fi
38-
export JAVA_HOME="${JDK_INSTALL_DIR}"
39-
export PATH="${JAVA_HOME}/bin:${PATH}"
40-
ACTIVE_JDK=$(java -version 2>&1 | head -1)
41-
# Check major version only — patch may differ from the Adoptium latest GA.
42-
if ! echo "${ACTIVE_JDK}" | grep -qE "\"${JDK_MAJOR}\."; then
43-
echo "FAIL:wrong JDK active (expected major ${JDK_MAJOR}, got: ${ACTIVE_JDK})" >&2
44-
exit 1
45-
fi
46-
47-
# Resolve ddprof.jar: prefer local build artifact, fall back to Maven snapshot.
48-
# Running mvn from /tmp avoids the empty pom.xml at the repo root.
49-
DDPROF_JAR_LOCAL=$(ls "${ROOT}/ddprof-lib/build/libs/ddprof-"*.jar 2>/dev/null | head -1)
50-
if [ -n "${DDPROF_JAR_LOCAL}" ] && [ -f "${DDPROF_JAR_LOCAL}" ]; then
51-
DDPROF_JAR="${DDPROF_JAR_LOCAL}"
52-
echo "Using local ddprof jar: ${DDPROF_JAR}"
53-
else
54-
if [ -z "${CURRENT_VERSION:-}" ]; then
55-
echo "FAIL:CURRENT_VERSION is empty and no local jar found (get-versions dotenv missing)" >&2
56-
exit 1
57-
fi
58-
echo "Local ddprof jar not found — downloading ${CURRENT_VERSION} from Maven snapshots"
59-
(cd /tmp && mvn org.apache.maven.plugins:maven-dependency-plugin:2.1:get \
60-
-DrepoUrl=https://central.sonatype.com/repository/maven-snapshots/ \
61-
-Dartifact=com.datadoghq:ddprof:${CURRENT_VERSION})
62-
DDPROF_JAR="/root/.m2/repository/com/datadoghq/ddprof/${CURRENT_VERSION}/ddprof-${CURRENT_VERSION}.jar"
63-
fi
64-
65-
if [ ! -f "${DDPROF_JAR}" ]; then
66-
echo "FAIL:ddprof jar unavailable" >&2
67-
exit 1
68-
fi
69-
70-
mkdir -p /var/lib/datadog
71-
wget -q --timeout=120 --tries=3 -O /var/lib/datadog/dd-java-agent.jar 'https://dtdg.co/latest-java-tracer'
72-
73-
# chaos.jar is produced once per pipeline by the chaos:build job (stresstest
74-
# stage) and pulled here as an artifact. Fall back to an inline build if the
75-
# artifact is absent (e.g. local repro outside CI).
76-
CHAOS_JAR="${ROOT}/ddprof-stresstest/build/libs/chaos.jar"
77-
if [ ! -f "${CHAOS_JAR}" ]; then
78-
echo "chaos.jar artifact not present — building inline"
79-
( cd "${ROOT}" && ./gradlew :ddprof-stresstest:chaosJar -q )
80-
fi
81-
82-
if [ ! -f "${CHAOS_JAR}" ]; then
83-
echo "FAIL:chaos.jar unavailable" >&2
84-
exit 1
85-
fi
86-
87-
# Patch dd-java-agent.jar with the locally built ddprof contents so the agent's
88-
# (relocated) profiler classes match the version under test.
89-
DD_AGENT_JAR=/var/lib/datadog/dd-java-agent.jar \
90-
DDPROF_JAR=${DDPROF_JAR} \
91-
OUTPUT_JAR=/var/lib/datadog/dd-java-agent-patched.jar \
92-
"${ROOT}/utils/patch-dd-java-agent.sh"
93-
94-
if [ ! -f /var/lib/datadog/dd-java-agent-patched.jar ]; then
95-
echo "FAIL:dd-java-agent patching failed" >&2
96-
exit 1
97-
fi
98-
99-
PATCHED_AGENT=/var/lib/datadog/dd-java-agent-patched.jar
100-
101-
case $CONFIG in
102-
profiler)
103-
echo "Running with profiler only"
104-
ENABLEMENT="-Ddd.profiling.enabled=true -Ddd.trace.enabled=false"
105-
# @Trace is a no-op without the tracer, so trace-context is excluded here.
106-
ANTAGONISTS="thread-churn,alloc-storm,vthread-churn,classloader-churn,bounded-pool,context-hop,consumer-group,hidden-class-churn,direct-memory,weakref-wave,dump-storm"
107-
;;
108-
profiler+tracer)
109-
echo "Running with profiler and tracer"
110-
ENABLEMENT="-Ddd.profiling.enabled=true -Ddd.trace.enabled=true"
111-
ANTAGONISTS="thread-churn,alloc-storm,vthread-churn,classloader-churn,trace-context,bounded-pool,context-hop,consumer-group,hidden-class-churn,direct-memory,weakref-wave,dump-storm"
112-
;;
113-
*)
114-
echo "Unknown configuration: $CONFIG"
115-
exit 1
116-
;;
117-
esac
118-
119-
case $ALLOCATOR in
120-
gmalloc)
121-
echo "Running with gmalloc"
122-
;;
123-
tcmalloc)
124-
echo "Running with tcmalloc"
125-
export LD_PRELOAD=$(find /usr/lib/ -name 'libtcmalloc_minimal.so.4')
126-
# thread-churn/dump-storm antagonists cycle many short-lived threads;
127-
# tcmalloc's defaults are slow to return their per-thread caches to the
128-
# OS, which was inflating container RSS past the OOM limit on aarch64.
129-
export TCMALLOC_RELEASE_RATE=10
130-
export TCMALLOC_AGGRESSIVE_DECOMMIT=1
131-
;;
132-
jemalloc)
133-
echo "Running with jemalloc"
134-
export LD_PRELOAD=$(find /usr/lib/ -name 'libjemalloc.so')
135-
;;
136-
*)
137-
echo "Unknown allocator: $ALLOCATOR"
138-
echo "Valid values are: gmalloc, tcmalloc, jemalloc"
139-
exit 1
140-
;;
141-
esac
142-
143-
echo "LD_PRELOAD=$LD_PRELOAD"
144-
145-
timeout "$((RUNTIME + 300))" \
146-
java -javaagent:${PATCHED_AGENT} \
147-
${ENABLEMENT} \
148-
-Ddd.profiling.upload.period=10 \
149-
-Ddd.profiling.start-force-first=true \
150-
-Ddd.profiling.ddprof.liveheap.enabled=true \
151-
-Ddd.profiling.ddprof.alloc.enabled=true \
152-
-Ddd.profiling.ddprof.wall.enabled=true \
153-
-Ddd.profiling.ddprof.nativemem.enabled=true \
154-
-Ddd.env=java-profiler-stability \
155-
-Ddd.service=java-profiler-chaos \
156-
-Xmx2g -Xms2g \
157-
-XX:MaxMetaspaceSize=384m \
158-
-XX:ErrorFile=${HERE}/../../hs_err.log \
159-
-XX:OnError="${HERE}/../../dd_crash_uploader.sh %p" \
160-
-jar ${CHAOS_JAR} \
161-
--duration ${RUNTIME}s \
162-
--antagonists ${ANTAGONISTS}
163-
164-
RC=$?
165-
echo "RC=$RC"
13+
export CHAOS_JDK_DIR="${CHAOS_JDK_DIR:-/opt/jdk-${CHAOS_JDK}}"
14+
export CHAOS_WORK_DIR="${CHAOS_WORK_DIR:-/var/lib/datadog}"
15+
export CHAOS_ERROR_FILE="${ROOT}/hs_err.log"
16616

167-
if [ $RC -ne 0 ]; then
168-
CRASH_MSG="Chaos harness crashed (RC=${RC})"
169-
HS_ERR="${HERE}/../../hs_err.log"
170-
if [ -f "${HS_ERR}" ]; then
171-
SIG=$(grep -m1 '^siginfo:' "${HS_ERR}" 2>/dev/null | tr -d '\n' | cut -c1-120)
172-
FRAME=$(grep -m1 'libjavaProfiler\|AsyncProfiler' "${HS_ERR}" 2>/dev/null | sed 's/^[[:space:]]*//' | tr -d '\n' | cut -c1-120)
173-
[ -n "${SIG}" ] && CRASH_MSG="${CRASH_MSG};${SIG}"
174-
[ -n "${FRAME}" ] && CRASH_MSG="${CRASH_MSG};${FRAME}"
175-
fi
176-
echo "FAIL:${CRASH_MSG}" >&2
177-
exit 1
178-
fi
17+
exec "${ROOT}/utils/run-chaos-harness.sh" "$@"

build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.datadoghq.profiler
33

44
import com.datadoghq.native.NativeBuildExtension
55
import com.datadoghq.native.model.BuildConfiguration
6+
import com.datadoghq.native.model.Platform
67
import com.datadoghq.native.util.PlatformUtils
78
import org.gradle.api.DefaultTask
89
import org.gradle.api.GradleException
@@ -204,6 +205,22 @@ class ProfilerTestPlugin : Plugin<Project> {
204205

205206
// Environment variables (explicit for consistency across both paths)
206207
val envVars = buildMap<String, String> {
208+
// Turn silent glibc heap corruption (e.g. a use-after-free write into a
209+
// freed chunk) into an immediate, attributable SIGABRT instead of a crash
210+
// much later in an unrelated allocation. Only meaningful against glibc's
211+
// own malloc: skip on musl (no MALLOC_CHECK_ support), and skip whenever a
212+
// sanitizer/allocator already replaces malloc via LD_PRELOAD.
213+
if (PlatformUtils.currentPlatform == Platform.LINUX &&
214+
!PlatformUtils.isMusl() &&
215+
!testEnv.containsKey("LD_PRELOAD")
216+
) {
217+
val perturbByte = (1..255).random()
218+
put("MALLOC_CHECK_", "3")
219+
put("MALLOC_PERTURB_", perturbByte.toString())
220+
// Logged so a glibc-detected corruption abort can be reproduced with the
221+
// same perturb byte (the value is otherwise random per run).
222+
project.logger.lifecycle("[$configName] MALLOC_PERTURB_=$perturbByte")
223+
}
207224
putAll(testEnv)
208225
put("DDPROF_TEST_DISABLE_RATE_LIMIT", "1")
209226
put("CI", (project.hasProperty("CI") || System.getenv("CI")?.toBoolean() ?: false).toString())

0 commit comments

Comments
 (0)