Skip to content

Static Analysis & Race Detection #130

Static Analysis & Race Detection

Static Analysis & Race Detection #130

name: Static Analysis & Race Detection
on:
push:
branches: [glucodroid]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [glucodroid]
paths-ignore:
- '**.md'
- 'docs/**'
workflow_dispatch:
schedule:
# Run nightly at 3 AM UTC (offset from UX QA at 2 AM)
- cron: '0 3 * * *'
env:
GRADLE_OPTS: -Xmx4g -XX:+HeapDumpOnOutOfMemoryError
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── 1. Android Lint ──────────────────────────────────────────────────────────
lint:
name: Android Lint (nogoogle release)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Run lint
run: |
./gradlew :Common:lintMobileLibre3SiDexNogoogleRelease \
--no-daemon --warning-mode=all
# abortOnError is false in build.gradle so this always exits 0;
# we parse severity ourselves below.
- name: Summarise lint results
if: always()
run: |
XML=$(find Common/build/reports -name "lint-results-mobileLibre3SiDexNogoogleRelease.xml" | head -1)
if [[ -z "$XML" ]]; then
echo "No lint XML found." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
python3 - "$XML" << 'PYEOF'
import xml.etree.ElementTree as ET, sys, os, collections
tree = ET.parse(sys.argv[1])
issues = tree.getroot().findall("issue")
by_sev = collections.defaultdict(list)
for iss in issues:
by_sev[iss.get("severity","?")].append(iss)
summary = os.environ.get("GITHUB_STEP_SUMMARY","")
lines = ["## Android Lint Results\n"]
lines.append(f"| Severity | Count |")
lines.append(f"|---|---|")
for sev in ("Error","Warning","Information","Hint"):
if sev in by_sev:
lines.append(f"| {sev} | {len(by_sev[sev])} |")
lines.append("")
# Thread-safety focused categories
RACE_CATS = {"VisibleForTests","WrongThread","NotThreadSafe",
"SynchronizedOrAtomicFieldUpdater","ThreadSafe"}
race_hits = [i for i in issues if i.get("id","") in RACE_CATS]
if race_hits:
lines.append("### Thread-Safety Findings\n")
for iss in race_hits[:30]:
loc = iss.find("location")
f = os.path.basename(loc.get("file","?")) if loc is not None else "?"
l = loc.get("line","?") if loc is not None else "?"
msg = iss.get("message","")[:160]
sev = iss.get("severity","?")
lines.append(f"- **[{sev}]** `{f}:{l}` — {msg}")
if len(race_hits) > 30:
lines.append(f"- … and {len(race_hits)-30} more")
lines.append("")
# Top-10 error categories
errors = by_sev.get("Error",[])
if errors:
lines.append("### Errors (top categories)\n")
cat_counts = collections.Counter(i.get("category","?") for i in errors)
for cat, cnt in cat_counts.most_common(10):
lines.append(f"- **{cat}**: {cnt}")
lines.append("")
if summary:
with open(summary, "a") as fh:
fh.write("\n".join(lines) + "\n")
else:
print("\n".join(lines))
# Exit 1 if any Error-severity issues exist
if by_sev.get("Error"):
print(f"::error::{len(by_sev['Error'])} lint error(s) found — see report artifact")
sys.exit(1)
PYEOF
- name: Upload lint reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lint-reports
path: |
Common/build/reports/lint-results-mobileLibre3SiDexNogoogleRelease.xml
Common/build/reports/lint-results-mobileLibre3SiDexNogoogleRelease.html
retention-days: 30
# ── 2. Race / Thread-Safety Detection ────────────────────────────────────────
race-detection:
name: Race & Thread-Safety Detection (API ${{ matrix.api-level }})
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
api-level: [31, 34]
fail-fast: false
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-race-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls -l /dev/kvm
- name: Build debug + test APKs (nogoogle)
run: |
./gradlew \
:Common:assembleMobileLibre3SiDexNogoogleDebug \
:Common:assembleMobileLibre3SiDexNogoogleDebugAndroidTest \
--no-daemon --warning-mode=all \
-Pno_x86 -Pno_x86_64
- name: Run on emulator — capture thread violations
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
profile: Nexus 6P
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
disk-size: 6000M
heap-size: 512M
script: bash .github/scripts/race-detection.sh
- name: Analyse logcat for race / thread violations
if: always()
run: |
python3 - << 'PYEOF'
import re, sys, os
logcat = open("logcat-full.log").read() if os.path.exists("logcat-full.log") else ""
instr = open("instrumentation.log").read() if os.path.exists("instrumentation.log") else ""
PATTERNS = {
"StrictMode violation": r"StrictMode policy violation",
"CalledFromWrongThread": r"CalledFromWrongThreadException",
"ConcurrentModification": r"ConcurrentModificationException",
"DeadObjectException": r"DeadObjectException",
"Main-thread IO": r"StrictMode.*disk (read|write)",
"ANR": r"Input dispatching timed out|ANR in",
"Looper not prepared": r"Can.t create handler inside thread.*Looper.prepare",
"IllegalStateException (thread)": r"IllegalStateException.*thread|thread.*IllegalStateException",
"Test failure": r"FAILURES!!|FAILED \(",
}
findings = {}
for label, pattern in PATTERNS.items():
hits = re.findall(f"(?m)^.*{pattern}.*$", logcat + "\n" + instr)
if hits:
findings[label] = hits
summary_path = os.environ.get("GITHUB_STEP_SUMMARY","")
lines = [f"## Race & Thread-Safety Scan — API ${{ matrix.api-level }}\n"]
if findings:
lines.append(f"**{len(findings)} category(ies) with findings:**\n")
for label, hits in findings.items():
lines.append(f"### {label} ({len(hits)} occurrence(s))")
for h in hits[:5]:
lines.append(f"```\n{h.strip()[:200]}\n```")
if len(hits) > 5:
lines.append(f"*… and {len(hits)-5} more — see logcat artifact*")
lines.append("")
else:
lines.append("No thread-safety violations or race indicators detected.\n")
# Note about StrictMode being commented out in Applic.java
lines.append("> **Note:** `StrictMode` is commented out in `Applic.java:792`.")
lines.append("> Uncommenting `startstrictmode()` would surface additional violations here.\n")
output = "\n".join(lines)
if summary_path:
with open(summary_path, "a") as fh:
fh.write(output + "\n")
print(output)
if findings:
critical = {k for k in findings if k not in ("Test failure",)}
if critical:
print(f"::warning::Thread-safety findings in {len(critical)} category(ies) — see step summary")
PYEOF
- name: Upload race-detection artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: race-detection-api${{ matrix.api-level }}
path: |
logcat-full.log
instrumentation.log
batterystats.txt
retention-days: 14
# ── 3. Redroid Kernel Probe (ARM64 runner) ───────────────────────────────────
# Checks whether GitHub's ubuntu-24.04-arm runners expose the binder and
# ashmem kernel modules required to run Redroid natively (no QEMU layer).
# This job never fails CI — it only reports findings so we know whether
# a Redroid-on-Actions path is viable.
redroid-kernel-probe:
name: Redroid Kernel Probe (ARM64)
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
continue-on-error: true
steps:
- name: Check kernel modules and Docker capabilities
run: |
echo "## Redroid Kernel Probe — \$(uname -r) [\$(uname -m)]" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
check() {
local label="$1" result="$2" detail="$3"
if [[ "$result" == "ok" ]]; then
echo "| ✅ | $label | \`$detail\` |" >> "$GITHUB_STEP_SUMMARY"
else
echo "| ❌ | $label | $detail |" >> "$GITHUB_STEP_SUMMARY"
fi
}
echo "| | Check | Detail |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY"
# binder
binder=$(grep binder /proc/filesystems 2>/dev/null | awk '{print $2}')
[[ -n "$binder" ]] && check "binder in /proc/filesystems" ok "$binder" \
|| check "binder in /proc/filesystems" fail "not found"
# ashmem
ashmem=$(grep ashmem /proc/misc 2>/dev/null | awk '{print $2}')
[[ -n "$ashmem" ]] && check "ashmem in /proc/misc" ok "$ashmem" \
|| check "ashmem in /proc/misc" fail "not found"
# /dev/binder device node
[[ -e /dev/binder ]] \
&& check "/dev/binder device node" ok "$(ls -l /dev/binder)" \
|| check "/dev/binder device node" fail "not present"
# /dev/ashmem device node
[[ -e /dev/ashmem ]] \
&& check "/dev/ashmem device node" ok "$(ls -l /dev/ashmem)" \
|| check "/dev/ashmem device node" fail "not present"
# modprobe availability (can we load modules?)
modprobe_out=$(modprobe binder_linux 2>&1 || true)
[[ -z "$modprobe_out" || "$modprobe_out" == *"already loaded"* ]] \
&& check "modprobe binder_linux" ok "loaded or already present" \
|| check "modprobe binder_linux" fail "$modprobe_out"
# Docker privileged mode (required by Redroid)
docker_ok=$(docker run --rm --privileged alpine uname -m 2>/dev/null || echo "failed")
[[ "$docker_ok" == "aarch64" ]] \
&& check "Docker privileged containers" ok "aarch64 confirmed" \
|| check "Docker privileged containers" fail "$docker_ok"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Kernel:** \$(uname -r) " >> "$GITHUB_STEP_SUMMARY"
echo "**Arch:** \$(uname -m)" >> "$GITHUB_STEP_SUMMARY"
# Print raw details to the log for debugging
echo "=== /proc/filesystems (android-related) ==="
grep -E "binder|ashmem" /proc/filesystems /proc/misc 2>/dev/null || echo "(none)"
echo "=== /dev entries ==="
ls -l /dev/binder /dev/ashmem 2>/dev/null || echo "(none)"
echo "=== kernel modules ==="
lsmod 2>/dev/null | grep -E "binder|ashmem" || echo "(none loaded)"
# ── 4. Gate ───────────────────────────────────────────────────────────────────
analysis-gate:
name: Analysis Gate
runs-on: ubuntu-latest
needs: [lint, race-detection, redroid-kernel-probe]
if: always()
steps:
- name: Check results
run: |
lint="${{ needs.lint.result }}"
race="${{ needs.race-detection.result }}"
probe="${{ needs.redroid-kernel-probe.result }}"
echo "Lint: $lint"
echo "Race detection: $race"
echo "Redroid kernel probe: $probe (informational only)"
# Race detection uses ::warning:: not exit 1, so failures here are
# genuine infrastructure problems, not finding warnings.
# Probe is continue-on-error so it never blocks the gate.
for r in "$lint" "$race"; do
if [[ "$r" == "failure" ]]; then
echo "One or more analysis jobs failed — check artifacts and step summaries."
exit 1
fi
done
echo "All analysis jobs completed."