Skip to content

Commit b9777c8

Browse files
committed
CI: build full RetroArch with ASan + UBSan and run headless smoke
Stands up a complementary CI job to the per-sample regression-test workflows shipped in v3-v6 (Linux-samples-gfx.yml). Builds full RetroArch with -fsanitize=address,undefined via the existing top- level Makefile SANITIZER= plumbing, then runs `./retroarch --help` and `./retroarch --features` under AddressSanitizer's abort-on-error mode and UBSan in soft-fail mode. The per-sample harnesses regression-test specific predicates that had bugs and by design only cover code that can be exercised by a small standalone test program with mocked dependencies. This job is complementary -- it sanitizer-instruments the entire RetroArch binary as it actually links and runs, so any future heap-corruption / use-after-free / signed-overflow / null-deref bug in code reachable from main() is caught for free as a side effect of any change. Same template as c82db9f's libretro-db samples build under sanitizer in CI, scaled up to the full retroarch binary. * .github/workflows/Linux-asan-ubsan.yml - Build matches Linux-Headless.yml's apt set and --disable-menu choice. Adds --disable-discord --disable-cheevos --disable-networking on the first iteration to shrink the third-party surface so that any sanitizer hit is a RetroArch- internal bug rather than noise from a vendored subsystem. Each can be re-enabled in follow-ups as the baseline stays green. - Build with `make SANITIZER=address,undefined`, which the top- level Makefile (line 153) propagates into CFLAGS / CXXFLAGS / LDFLAGS for every TU and the final link. No new apt packages: libasan / libubsan ship with the default gcc on ubuntu-latest. - Smoke runs --help and --features. Both exit(0) directly after printing. Coverage scope is honest in the inline comment: libc init, main(), frontend driver bootstrap, argv duplication, the getopt walk over the full option table, the print functions themselves, and the static feature-table walks --features performs. Doesn't reach core loading, video init, or cleanup- on-shutdown -- a follow-up step running with --max-frames=N against a noop core can extend coverage to the full lifecycle, but adding the noop-core dependency to CI is a separate step better handled in its own patch. - Sanitizer settings: ASAN_OPTIONS=abort_on_error=1:detect_leaks=0: print_stacktrace=1:strict_string_checks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 LSAN_OPTIONS=exitcode=0 ASan in abort-on-error: any heap corruption fails the job immediately. Leak detection off for now -- libGL / Mesa / Wayland symbol-resolution drives a non-trivial baseline of "leaks" at process shutdown that aren't RetroArch bugs; can revisit with a suppression file once the rest is clean. UBSan in report-only: the first iteration is a discovery run. Pre-existing signed-overflow / alignment / shift-too-large diagnostics in audio resampling, color math, and vendored deps are likely to be present. The "Summarize UBSan baseline" step prints the deduplicated list so follow-up patches have a target to drive to zero. Once that count reaches zero, flip halt_on_error=1 in the smoke steps to enforce. Each smoke step independently greps stderr for "AddressSanitizer:" and exits 1 on any match -- belt-and- suspenders to abort_on_error in case a future ASan release changes its default exit semantics. - Kept as its own workflow rather than folded into Linux-Headless so the existing green-path build smoke stays fast (no ~3-5x ASan slowdown), and so this one can own its longer 25-minute timeout independently. - Trigger matches the per-sample workflows: push to master, pull_request to master, workflow_dispatch.
1 parent aa4a196 commit b9777c8

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
name: CI Linux ASan + UBSan [No Menu]
2+
3+
# Builds full RetroArch with -fsanitize=address,undefined and runs
4+
# headless smoke invocations (--help, --features) so AddressSanitizer
5+
# and UndefinedBehaviorSanitizer instrument the startup config-loading,
6+
# argument parsing, default-driver init, and cleanup-on-exit paths.
7+
#
8+
# The per-sample tests under .github/workflows/Linux-samples-gfx.yml,
9+
# Linux-samples-tasks.yml, and Linux-libretro-{db,common}-samples.yml
10+
# regression-test specific predicates that previously had bugs. This
11+
# job is complementary -- it covers everything those harnesses can't
12+
# reach because the code only runs from main(), and catches future
13+
# heap-corruption / UB regressions across the whole code base for
14+
# free as a side effect of any change that can be exercised by a
15+
# headless run.
16+
#
17+
# Build configuration matches Linux-Headless.yml (--disable-menu) with
18+
# additional --disable-discord --disable-cheevos --disable-networking
19+
# to shrink the third-party surface on this first iteration. Each can
20+
# be re-enabled once the baseline is green.
21+
22+
on:
23+
push:
24+
branches:
25+
- master
26+
pull_request:
27+
branches:
28+
- master
29+
workflow_dispatch:
30+
31+
permissions:
32+
contents: read
33+
34+
env:
35+
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
36+
37+
jobs:
38+
asan-ubsan:
39+
name: Build with ASan+UBSan and run headless smoke
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 25
42+
43+
steps:
44+
- name: Install dependencies
45+
# Mirrors Linux-Headless.yml's apt set so the build matches a
46+
# known-good headless configuration. No sanitizer-specific
47+
# packages required; libasan / libubsan ship with the gcc that
48+
# ubuntu-latest has installed by default.
49+
run: |
50+
sudo apt-get update -y
51+
sudo apt-get install -y \
52+
build-essential \
53+
libxkbcommon-dev libx11-xcb-dev \
54+
zlib1g-dev libfreetype6-dev \
55+
libegl1-mesa-dev libgles2-mesa-dev libgbm-dev \
56+
nvidia-cg-toolkit nvidia-cg-dev \
57+
libavcodec-dev libsdl2-dev libsdl-image1.2-dev \
58+
libxml2-dev yasm
59+
60+
- name: Checkout
61+
uses: actions/checkout@v3
62+
63+
- name: Configure (no menu, no discord/cheevos/networking)
64+
# Trim the build surface for the first iteration so any
65+
# sanitizer hit is a RetroArch-internal bug rather than noise
66+
# from a vendored third-party subsystem. The disabled
67+
# subsystems will be re-enabled in follow-up patches as the
68+
# baseline stays green.
69+
run: |
70+
./configure \
71+
--disable-menu \
72+
--disable-discord \
73+
--disable-cheevos \
74+
--disable-networking
75+
76+
- name: Build with -fsanitize=address,undefined
77+
# The top-level Makefile (line 153) propagates SANITIZER into
78+
# CFLAGS / CXXFLAGS / LDFLAGS for every translation unit and
79+
# the final link. ASan defaults to abort-on-heap-corruption;
80+
# UBSan recovery is controlled at runtime via UBSAN_OPTIONS
81+
# (see the smoke-run steps below).
82+
run: |
83+
make -j$(getconf _NPROCESSORS_ONLN) \
84+
SANITIZER=address,undefined
85+
test -x retroarch
86+
file retroarch
87+
88+
- name: Smoke run --help (ASan strict, UBSan soft-fail)
89+
# `--help` exits cleanly via exit(0) after printing the usage
90+
# banner. Coverage scope: libc init, main(), frontend
91+
# driver bootstrap, argv duplication, the getopt walk over
92+
# the full option table, and retroarch_print_help() itself.
93+
# Doesn't reach into core loading / video init / cleanup-on-
94+
# shutdown -- a follow-up step running with --max-frames=N
95+
# against a noop core will extend coverage to the full
96+
# lifecycle. ASan runs in abort-on-error mode so any heap
97+
# corruption fails the job; UBSan reports stacktraces but
98+
# does not fail (first iteration -- we want to see the
99+
# baseline before deciding which UBSan classes to enforce).
100+
env:
101+
ASAN_OPTIONS: abort_on_error=1:detect_leaks=0:print_stacktrace=1:strict_string_checks=1
102+
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=0
103+
# Avoid noise from libGL / Mesa / Wayland symbol-resolution
104+
# leaks at process shutdown; those are not RetroArch bugs.
105+
LSAN_OPTIONS: exitcode=0
106+
run: |
107+
set -eu
108+
timeout 30 ./retroarch --help > /tmp/help.out 2> /tmp/help.err || rc=$?
109+
echo "exit=${rc:-0}"
110+
echo "=== stdout (head) ==="
111+
head -40 /tmp/help.out
112+
echo "=== stderr ==="
113+
cat /tmp/help.err
114+
# ASan abort_on_error sends the process to exit code 1 + a
115+
# SUMMARY: line on stderr. Treat any "AddressSanitizer:"
116+
# marker as fatal regardless of exit code.
117+
if grep -q "AddressSanitizer:" /tmp/help.err; then
118+
echo "[FAIL] AddressSanitizer reported a finding"
119+
exit 1
120+
fi
121+
echo "[pass] retroarch --help under ASan+UBSan"
122+
123+
- name: Smoke run --features (ASan strict, UBSan soft-fail)
124+
# `--features` exits cleanly via exit(0) after printing the
125+
# compile-time feature list. Coverage scope is the same as
126+
# --help (libc init / main / frontend bootstrap / arg parse)
127+
# plus retroarch_print_features(), which walks the static
128+
# video / audio / input / camera / location / record / cheats
129+
# / network / database / overlay / hid feature tables. Each
130+
# such walk reads pointers into a static-string table -- low
131+
# corruption surface but a useful sanity check that the link-
132+
# time feature flags resolve consistently. Doesn't reach
133+
# core loading or cleanup-on-shutdown.
134+
env:
135+
ASAN_OPTIONS: abort_on_error=1:detect_leaks=0:print_stacktrace=1:strict_string_checks=1
136+
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=0
137+
LSAN_OPTIONS: exitcode=0
138+
run: |
139+
set -eu
140+
timeout 30 ./retroarch --features > /tmp/features.out 2> /tmp/features.err || rc=$?
141+
echo "exit=${rc:-0}"
142+
echo "=== stdout (head) ==="
143+
head -40 /tmp/features.out
144+
echo "=== stderr ==="
145+
cat /tmp/features.err
146+
if grep -q "AddressSanitizer:" /tmp/features.err; then
147+
echo "[FAIL] AddressSanitizer reported a finding"
148+
exit 1
149+
fi
150+
echo "[pass] retroarch --features under ASan+UBSan"
151+
152+
- name: Summarize UBSan baseline
153+
# Both runtime steps are non-fatal on UBSan diagnostics. This
154+
# step prints a count of distinct UBSan messages observed so
155+
# follow-up patches have a numerical target to drive to zero.
156+
# If this step ever shows zero, flip
157+
# UBSAN_OPTIONS=halt_on_error=1 in the runtime steps above to
158+
# enforce the clean baseline.
159+
if: always()
160+
run: |
161+
set +e
162+
ub_count=$( {
163+
grep -h "runtime error:" /tmp/help.err /tmp/features.err 2>/dev/null
164+
} | sort -u | wc -l)
165+
echo "UBSan distinct diagnostics: ${ub_count}"
166+
if [ "${ub_count}" != "0" ]; then
167+
echo "=== unique UBSan diagnostics ==="
168+
grep -h "runtime error:" /tmp/help.err /tmp/features.err 2>/dev/null \
169+
| sort -u
170+
fi

0 commit comments

Comments
 (0)