Skip to content

Commit 45aeee4

Browse files
committed
CI: extend ASan+UBSan workflow with imageviewer headless smoke
Follow-up to b9777c8 (the original ASan+UBSan workflow) and 70f24be (v8: flip to halt-on-error after a clean baseline). Adds a third smoke tier that actually loads a libretro core and runs it through ~5 seconds of real frame dispatch + clean shutdown, covering everything --help and --features can't reach: dlopen of a core, retro_load_game, the stb_image-driven decode path, the X11 + XVideo color-conversion + shared-memory image- transfer pipeline, the runloop, and full cleanup-on-shutdown. The new step: 1. Builds cores/libretro-imageviewer/image_core.so with the same SANITIZER=address,undefined the main retroarch binary uses -- ensures stb_image's stack-buffer-overflow / use-after- return surface is fully instrumented (the global ASan allocator interceptor catches heap bugs across module boundaries regardless, but stack instrumentation requires per-TU compilation). This works because v9 (efae310) made the standalone Makefile sanitizer-aware. 2. Generates an 8x8 solid-red test PNG via a Python heredoc using only struct + zlib -- 75 bytes, no apt dependency on imagemagick or similar (Python ships on every ubuntu-latest). 3. Spins up Xvfb on display :99 with a 320x240x24 screen (small to minimise X server memory footprint), waits for the XVideo extension to be available, writes a minimal retroarch.cfg that pins the video driver to "xvideo" and the audio driver to "null" (no PulseAudio / ALSA dependency on the runner), and runs: DISPLAY=:99 ./retroarch \ -c /tmp/asan-cfg/retroarch.cfg \ -L cores/libretro-imageviewer/image_core.so \ /tmp/test.png \ --max-frames=300 \ --verbose --max-frames=300 = ~5s nominal at 60fps, ~15-25s under sanitizer overhead. Crucially, --max-frames triggers a clean exit through the normal runloop teardown -- not a SIGTERM mid-execution -- so cleanup paths are sanitizer- instrumented too. Wall-clock timeout(1) at 60s as a safety backstop. 4. Surfaces sanitizer findings (AddressSanitizer: / runtime error:) in the step output as informational text only. The step is soft-fail (continue-on-error: true) on this first iteration because lots of things can fire here that aren't RetroArch bugs (Xvfb quirks, libGL / Mesa software-rasterizer leaks at shutdown, X11 driver init noise) and forcing strict enforcement before measuring a baseline would block merges on noise. Once the baseline is characterised the same way the --help step's was, this step can be flipped to strict. Why xvideo specifically: it's the smallest self-contained X11 video driver in the tree (1163 LOC adapted from bSNES / MPlayer), no GL dependency, real YUV color conversion + XShm transfer, and Xvfb exposes the XVideo extension by default. null video would be simpler but skips the entire pixel-data path, which is exactly the surface most likely to fire under sanitizer. * .github/workflows/Linux-asan-ubsan.yml - Install dependencies: add xvfb, x11-utils, libxv-dev, libxext-dev, libxxf86vm-dev. No new compiler/runtime dependencies; the X11 transitive dev headers are needed for xvideo build, xvfb + x11-utils for the runtime invocation. - Configure: add --enable-xvideo to make xvideo a hard build requirement -- silent fall-back to a different driver would skew the smoke's coverage without warning. - New steps appended after --features: * Build imageviewer core under ASan + UBSan * Generate test PNG for imageviewer * Smoke run imageviewer headless under Xvfb (soft-fail) - Header comment + --help comment updated to describe the three-tier coverage structure (--help, --features, imageviewer). Verified locally on efae310: - YAML parses with 9 steps, soft-fail correctly scoped to only the new imageviewer step. - Imageviewer core builds clean with SANITIZER=address,undefined (build step minus apt installs reproduced verbatim locally). - Test PNG generation runs clean, produces a valid 75-byte 8x8 PNG (verified by `file` and stb_image acceptance). - Xvfb on a fresh ubuntu-latest equivalent exposes the XVideo and MIT-SHM extensions out-of-the-box. - xdpyinfo -queryExtensions emits extension names with leading whitespace, hence the ^[[:space:]]+ in the detection regex (a naive ^XVideo / ^MIT-SHM regex would silently fail to match -- caught and fixed during pre-flight). Cannot verify locally: the actual end-to-end run, which would require building a sanitizer-instrumented retroarch binary against a full apt set. The first CI run is the experiment, the same way the original workflow's was.
1 parent efae310 commit 45aeee4

1 file changed

Lines changed: 212 additions & 16 deletions

File tree

.github/workflows/Linux-asan-ubsan.yml

Lines changed: 212 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
name: CI Linux ASan + UBSan [No Menu]
22

33
# 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.
4+
# headless smoke invocations. Three coverage tiers, in increasing
5+
# order of how much of the binary they actually instrument:
6+
#
7+
# 1. --help and --features. Exit(0) directly after printing.
8+
# Coverage scope: libc init, main(), frontend driver bootstrap,
9+
# argv duplication, the getopt walk, and the print functions.
10+
# Strict ASan + UBSan -- baseline confirmed clean (run #1).
11+
#
12+
# 2. Headless imageviewer core under Xvfb + xvideo video driver +
13+
# null audio driver, --max-frames=300 for a clean shutdown
14+
# through the normal runloop teardown. Exercises core loading
15+
# via dlopen, the imageviewer's stb_image-driven loader, the
16+
# X11 + XVideo color-conversion pipeline, the runloop, and
17+
# cleanup-on-shutdown. Soft-fail (continue-on-error) on this
18+
# first iteration: lots can go wrong (Xvfb quirks, libGL leaks,
19+
# driver init noise) that aren't RetroArch bugs but would fail
20+
# the run if treated strictly. Sanitizer findings are still
21+
# surfaced in step output for triage.
722
#
823
# The per-sample tests under .github/workflows/Linux-samples-gfx.yml,
924
# Linux-samples-tasks.yml, and Linux-libretro-{db,common}-samples.yml
@@ -43,14 +58,23 @@ jobs:
4358
steps:
4459
- name: Install dependencies
4560
# 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.
61+
# known-good headless configuration. Adds:
62+
# xvfb / x11-utils -- virtual X server for the imageviewer
63+
# smoke step + xdpyinfo for diagnostics
64+
# libxv-dev -- XVideo extension headers, for the
65+
# xvideo video driver
66+
# libxext-dev / libxxf86vm-dev -- transitive X11 deps that
67+
# configure's xvideo check requires
68+
# No sanitizer-specific packages required; libasan / libubsan
69+
# ship with the gcc that ubuntu-latest has installed by
70+
# default.
4971
run: |
5072
sudo apt-get update -y
5173
sudo apt-get install -y \
5274
build-essential \
5375
libxkbcommon-dev libx11-xcb-dev \
76+
libxv-dev libxext-dev libxxf86vm-dev \
77+
xvfb x11-utils \
5478
zlib1g-dev libfreetype6-dev \
5579
libegl1-mesa-dev libgles2-mesa-dev libgbm-dev \
5680
nvidia-cg-toolkit nvidia-cg-dev \
@@ -60,18 +84,23 @@ jobs:
6084
- name: Checkout
6185
uses: actions/checkout@v3
6286

63-
- name: Configure (no menu, no discord/cheevos/networking)
87+
- name: Configure (no menu, no discord/cheevos/networking, xvideo on)
6488
# Trim the build surface for the first iteration so any
6589
# sanitizer hit is a RetroArch-internal bug rather than noise
6690
# from a vendored third-party subsystem. The disabled
6791
# subsystems will be re-enabled in follow-up patches as the
68-
# baseline stays green.
92+
# baseline stays green. --enable-xvideo is explicit because
93+
# the imageviewer smoke step below selects xvideo as the video
94+
# driver; if its build deps were ever missing, a silent fall-
95+
# back to a different driver would skew the smoke's coverage
96+
# without warning.
6997
run: |
7098
./configure \
7199
--disable-menu \
72100
--disable-discord \
73101
--disable-cheevos \
74-
--disable-networking
102+
--disable-networking \
103+
--enable-xvideo
75104
76105
- name: Build with -fsanitize=address,undefined
77106
# The top-level Makefile (line 153) propagates SANITIZER into
@@ -91,13 +120,12 @@ jobs:
91120
# driver bootstrap, argv duplication, the getopt walk over
92121
# the full option table, and retroarch_print_help() itself.
93122
# 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 and UBSan both run in halt-on-error mode:
97-
# the first run of this workflow (Apr 28 2026, run #1)
98-
# reported zero distinct UBSan diagnostics across both
99-
# smoke invocations, so the baseline for these two surfaces
100-
# is clean and we enforce it. If a future change introduces
123+
# shutdown; the imageviewer smoke step below covers those.
124+
# ASan and UBSan both run in halt-on-error mode: the first
125+
# run of this workflow (Apr 28 2026, run #1) reported zero
126+
# distinct UBSan diagnostics across both --help and
127+
# --features, so the baseline for these two surfaces is
128+
# clean and we enforce it. If a future change introduces
101129
# signed-overflow / alignment / shift-too-large UB along
102130
# the option-parsing or print paths, this step will fail
103131
# and the diagnostic line will be in the captured stderr.
@@ -164,3 +192,171 @@ jobs:
164192
exit 1
165193
fi
166194
echo "[pass] retroarch --features under ASan+UBSan"
195+
196+
- name: Build imageviewer core under ASan + UBSan
197+
# The standalone Makefile under cores/libretro-imageviewer/
198+
# honours the same SANITIZER= knob as the top-level Makefile
199+
# (added in the v9 Makefile cleanup, efae310). Building the
200+
# core with sanitizers enabled means the stb_image-driven
201+
# decode path is fully instrumented when RetroArch dlopens
202+
# it -- ASan would otherwise only catch heap corruption via
203+
# the global allocator interceptor and would miss stack-
204+
# buffer-overflow / use-after-return inside stb_image
205+
# itself. Same SANITIZER= value as the main build.
206+
run: |
207+
set -eu
208+
cd cores/libretro-imageviewer
209+
make clean
210+
make SANITIZER=address,undefined
211+
test -x image_core.so
212+
file image_core.so
213+
214+
- name: Generate test PNG for imageviewer
215+
# Tiny 8x8 solid-red PNG, 75 bytes. Hand-rolled via Python
216+
# struct + zlib to avoid an apt dependency on imagemagick or
217+
# similar. Python ships on every ubuntu-latest by default.
218+
# Saved under /tmp because the workspace lives on a path
219+
# GitHub Actions cleans up post-run anyway.
220+
run: |
221+
set -eu
222+
python3 - <<'PY'
223+
import struct, zlib
224+
def chunk(name, data):
225+
crc = zlib.crc32(name + data)
226+
return struct.pack('>I', len(data)) + name + data + \
227+
struct.pack('>I', crc)
228+
sig = b'\x89PNG\r\n\x1a\n'
229+
ihdr = chunk(b'IHDR',
230+
struct.pack('>IIBBBBB', 8, 8, 8, 2, 0, 0, 0))
231+
raw = b''
232+
for _ in range(8):
233+
raw += b'\x00' + b'\xff\x00\x00' * 8
234+
idat = chunk(b'IDAT', zlib.compress(raw))
235+
iend = chunk(b'IEND', b'')
236+
with open('/tmp/test.png', 'wb') as f:
237+
f.write(sig + ihdr + idat + iend)
238+
PY
239+
file /tmp/test.png
240+
241+
- name: Smoke run imageviewer headless under Xvfb (soft-fail)
242+
# Loads cores/libretro-imageviewer/image_core.so against a
243+
# tiny PNG, runs --max-frames=300 (~5s nominal at 60fps,
244+
# ~15-25s under sanitizer overhead), then exits via the
245+
# normal runloop teardown path. Covers what the --help and
246+
# --features smokes can't: dlopen of a libretro core,
247+
# retro_load_game, the stb_image decode path, the X11 +
248+
# XVideo color-conversion + shared-memory image-transfer
249+
# pipeline, the runloop, and full cleanup-on-shutdown.
250+
#
251+
# Soft-fail (continue-on-error: true) on this first
252+
# iteration. Reasoning: lots can go wrong here that aren't
253+
# RetroArch bugs -- Xvfb quirks, libGL / Mesa software-
254+
# rasterizer leaks at shutdown, X11 driver init noise --
255+
# and forcing the workflow to enforce strict cleanliness
256+
# before we've measured the baseline would block merges on
257+
# noise. Sanitizer findings ARE still surfaced in the step
258+
# output for triage; they just don't fail the job. Once
259+
# the baseline is characterised the same way the --help
260+
# step's was, this step can be flipped to strict.
261+
#
262+
# Audio driver is "null" (no PulseAudio / ALSA dependency
263+
# on the runner). Video driver is "xvideo" because Xvfb
264+
# exposes the XVideo extension and that's the smallest /
265+
# most self-contained X11 driver in the tree (1163 LOC,
266+
# adapted from bSNES/MPlayer, no GL dependency). Verbose
267+
# output is on so the run captures the full log for triage.
268+
continue-on-error: true
269+
env:
270+
# detect_leaks=0 because libGL / Mesa symbol-resolution
271+
# plus X11 connection caches produce non-trivial leaks at
272+
# process shutdown that aren't RetroArch bugs. Can be
273+
# flipped on later with a suppression file. Heap
274+
# corruption / UAF / double-free still abort under
275+
# abort_on_error=1.
276+
ASAN_OPTIONS: abort_on_error=1:detect_leaks=0:print_stacktrace=1:strict_string_checks=1
277+
# halt_on_error=0 so a single signed-overflow somewhere
278+
# in the runloop doesn't truncate the stderr log before
279+
# we see the full picture. The grep below still
280+
# surfaces every "runtime error:" line for triage.
281+
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=0
282+
LSAN_OPTIONS: exitcode=0
283+
run: |
284+
set -eu
285+
286+
# Start Xvfb on display :99. -screen geometry is small
287+
# because the imageviewer doesn't care about resolution
288+
# and we're trying to minimise X server memory. +extension
289+
# GLX is implicit in modern Xvfb; XVideo is exposed by
290+
# default.
291+
Xvfb :99 -screen 0 320x240x24 -nolisten tcp &
292+
XVFB_PID=$!
293+
# Ensure cleanup on any exit (including soft-fail ones).
294+
trap "kill $XVFB_PID 2>/dev/null || true" EXIT
295+
# Give Xvfb time to come up; xdpyinfo also confirms
296+
# XVideo is available before we waste cycles trying to
297+
# use it. xdpyinfo prints extension names indented with
298+
# leading whitespace, hence the ^[[:space:]]* in the
299+
# regex.
300+
for i in 1 2 3 4 5; do
301+
if DISPLAY=:99 xdpyinfo -queryExtensions 2>/dev/null \
302+
| grep -qE "^[[:space:]]+XVideo\b"; then
303+
break
304+
fi
305+
sleep 1
306+
done
307+
DISPLAY=:99 xdpyinfo -queryExtensions \
308+
| grep -E "^[[:space:]]+(XVideo|MIT-SHM)\b" || true
309+
310+
# Minimal config: xvideo video, null audio, one frame of
311+
# logging detail. Everything else takes built-in defaults.
312+
mkdir -p /tmp/asan-cfg
313+
cat > /tmp/asan-cfg/retroarch.cfg <<EOF
314+
video_driver = "xvideo"
315+
audio_driver = "null"
316+
video_threaded = "false"
317+
video_fullscreen = "false"
318+
video_windowed_fullscreen = "false"
319+
EOF
320+
321+
# Run. --max-frames triggers a clean shutdown via the
322+
# normal runloop teardown after N frames, which is what
323+
# we want for cleanup-path coverage. Wall-clock timeout
324+
# is a safety net at 60s; --max-frames at 300 should
325+
# finish in well under that even with sanitizer overhead.
326+
set +e
327+
DISPLAY=:99 timeout 60 ./retroarch \
328+
-c /tmp/asan-cfg/retroarch.cfg \
329+
-L cores/libretro-imageviewer/image_core.so \
330+
/tmp/test.png \
331+
--max-frames=300 \
332+
--verbose \
333+
> /tmp/imageviewer.out 2> /tmp/imageviewer.err
334+
rc=$?
335+
set -e
336+
337+
echo "exit=${rc}"
338+
echo "=== stdout (last 80) ==="
339+
tail -80 /tmp/imageviewer.out || true
340+
echo "=== stderr (last 200) ==="
341+
tail -200 /tmp/imageviewer.err || true
342+
343+
# Surface sanitizer findings explicitly (informational --
344+
# we do NOT exit 1 because this step is soft-fail). Once
345+
# the baseline is characterised, this section will gate
346+
# on findings the same way the --help / --features steps
347+
# do.
348+
if grep -q "AddressSanitizer:" /tmp/imageviewer.err; then
349+
echo ""
350+
echo "[INFO] AddressSanitizer reported finding(s):"
351+
grep -A 2 "AddressSanitizer:" /tmp/imageviewer.err \
352+
| head -40
353+
fi
354+
ub_count=$(grep -c "runtime error:" /tmp/imageviewer.err \
355+
2>/dev/null || true)
356+
if [ "${ub_count:-0}" != "0" ]; then
357+
echo ""
358+
echo "[INFO] UBSan reported ${ub_count} runtime error(s):"
359+
grep -h "runtime error:" /tmp/imageviewer.err \
360+
| sort -u | head -20
361+
fi
362+
echo "[done] imageviewer headless smoke (soft-fail)"

0 commit comments

Comments
 (0)