mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
Built with ffmpeg 8, libaom 3.13, x265 4.1, glibc 2.43 and clang 21, five lanes failed on the 26.04 runners: - AV1 fixtures: a monochrome sequence comes out full range with no matrix whatever is asked for; the oracle takes the colour ffprobe reads instead of assuming it. - Preview audio oracle: ffmpeg 8 leaves a timestamp gap at every seam of a -stream_loop copy, which the mixer honours and ffmpeg's raw decode does not. trailing_large loops its picture and encodes its sound as one track. - rh265 multi-slice oracle: x265 4.1 writes unreadable slice headers when two slices are encoded on several threads; those cases encode on one worker. - vkms: the kernel follows the runner's own release, its modules may sit under /usr/lib/modules, and modules-extra is fetched only where it exists. - clang -std=c89 -pedantic: glibc's const-generic strchr/strstr under _GNU_SOURCE are _Generic; -Wno-c11-extensions goes wherever -Wno-c99-extensions already does. ci_paths filters a file at the top of a samples tree by its own path. rh264 and rh265 lose the parameters no instantiation reads, the 8-bit H.264 kernels mark the depth they ignore, and the AV1 WebM fixtures leave out the MP4 box writer they never use.
2365 lines
121 KiB
YAML
2365 lines
121 KiB
YAML
name: CI Linux samples/tasks
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
# ci_paths: generated by tools/ci_paths.py, do not edit
|
|
paths:
|
|
- '**'
|
|
- '!samples/**'
|
|
- '!libretro-common/samples/**'
|
|
- '!.github/workflows/*.yml'
|
|
- 'libretro-common/samples/encodings/rbzip2/**'
|
|
- 'samples/menu/dirwalk/**'
|
|
- 'samples/playlist/playlist_parity_test.c'
|
|
- 'samples/tasks/autodetect_handoff/**'
|
|
- 'samples/tasks/bluetooth_scan/**'
|
|
- 'samples/tasks/bps_patch/**'
|
|
- 'samples/tasks/bsv_checkpoint/**'
|
|
- 'samples/tasks/bsv_replay/**'
|
|
- 'samples/tasks/bsv_replay_init/**'
|
|
- 'samples/tasks/bsv_statestream/**'
|
|
- 'samples/tasks/cloudsync/**'
|
|
- 'samples/tasks/content_prefetch/**'
|
|
- 'samples/tasks/core_backup/**'
|
|
- 'samples/tasks/core_updater/**'
|
|
- 'samples/tasks/database/**'
|
|
- 'samples/tasks/decompress/**'
|
|
- 'samples/tasks/file_transfer/**'
|
|
- 'samples/tasks/http/**'
|
|
- 'samples/tasks/http_emscripten/**'
|
|
- 'samples/tasks/image_decode_slice/**'
|
|
- 'samples/tasks/input_remap/**'
|
|
- 'samples/tasks/natt_desc/**'
|
|
- 'samples/tasks/netplay_nat/**'
|
|
- 'samples/tasks/ordering/**'
|
|
- 'samples/tasks/overlay/**'
|
|
- 'samples/tasks/patch_stream/**'
|
|
- 'samples/tasks/pl_thumbnail/**'
|
|
- 'samples/tasks/playlist_manager/**'
|
|
- 'samples/tasks/save_state/**'
|
|
- 'samples/tasks/turbo_bind/**'
|
|
- 'samples/tasks/video_shader_parse/**'
|
|
- 'samples/tasks/wait_timeout/**'
|
|
- 'samples/tasks/watchdog/**'
|
|
- 'samples/tasks/wifi_tasks/**'
|
|
- 'samples/playlist/**'
|
|
- 'samples/ui_companion_log_stub.c'
|
|
- '.github/workflows/Linux-samples-tasks.yml'
|
|
# ci_paths: end
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
# ci_paths: generated by tools/ci_paths.py, do not edit
|
|
paths:
|
|
- '**'
|
|
- '!samples/**'
|
|
- '!libretro-common/samples/**'
|
|
- '!.github/workflows/*.yml'
|
|
- 'libretro-common/samples/encodings/rbzip2/**'
|
|
- 'samples/menu/dirwalk/**'
|
|
- 'samples/playlist/playlist_parity_test.c'
|
|
- 'samples/tasks/autodetect_handoff/**'
|
|
- 'samples/tasks/bluetooth_scan/**'
|
|
- 'samples/tasks/bps_patch/**'
|
|
- 'samples/tasks/bsv_checkpoint/**'
|
|
- 'samples/tasks/bsv_replay/**'
|
|
- 'samples/tasks/bsv_replay_init/**'
|
|
- 'samples/tasks/bsv_statestream/**'
|
|
- 'samples/tasks/cloudsync/**'
|
|
- 'samples/tasks/content_prefetch/**'
|
|
- 'samples/tasks/core_backup/**'
|
|
- 'samples/tasks/core_updater/**'
|
|
- 'samples/tasks/database/**'
|
|
- 'samples/tasks/decompress/**'
|
|
- 'samples/tasks/file_transfer/**'
|
|
- 'samples/tasks/http/**'
|
|
- 'samples/tasks/http_emscripten/**'
|
|
- 'samples/tasks/image_decode_slice/**'
|
|
- 'samples/tasks/input_remap/**'
|
|
- 'samples/tasks/natt_desc/**'
|
|
- 'samples/tasks/netplay_nat/**'
|
|
- 'samples/tasks/ordering/**'
|
|
- 'samples/tasks/overlay/**'
|
|
- 'samples/tasks/patch_stream/**'
|
|
- 'samples/tasks/pl_thumbnail/**'
|
|
- 'samples/tasks/playlist_manager/**'
|
|
- 'samples/tasks/save_state/**'
|
|
- 'samples/tasks/turbo_bind/**'
|
|
- 'samples/tasks/video_shader_parse/**'
|
|
- 'samples/tasks/wait_timeout/**'
|
|
- 'samples/tasks/watchdog/**'
|
|
- 'samples/tasks/wifi_tasks/**'
|
|
- 'samples/playlist/**'
|
|
- 'samples/ui_companion_log_stub.c'
|
|
- '.github/workflows/Linux-samples-tasks.yml'
|
|
# ci_paths: end
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
|
|
|
jobs:
|
|
samples-tasks:
|
|
name: Build and run samples/tasks
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
APT_PKGS: build-essential zlib1g-dev gcc-mingw-w64-x86-64 wine64 ccache
|
|
# One job runs every samples/tasks oracle, most of them three times
|
|
# (plain, ASan + UBSan, TSan): 53 steps. Ten minutes was the budget
|
|
# when there were fewer; it now runs out partway through and the
|
|
# job is cancelled with nothing failing.
|
|
timeout-minutes: 30
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Apt cache key
|
|
id: apt
|
|
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
|
|
|
|
- name: Restore apt cache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: ~/.apt-cache
|
|
key: ${{ steps.apt.outputs.key }}
|
|
restore-keys: ${{ steps.apt.outputs.prefix }}
|
|
|
|
- name: Install dependencies
|
|
timeout-minutes: 20
|
|
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
|
|
|
|
- name: Save apt cache
|
|
if: env.APT_CACHE_SAVE == '1'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: ~/.apt-cache
|
|
key: ${{ steps.apt.outputs.key }}
|
|
|
|
- name: Link with lld
|
|
run: sh .github/workflows/scripts/use-lld.sh
|
|
|
|
- name: Restore ccache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: ~/.ccache
|
|
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
|
|
restore-keys: |
|
|
ccache-${{ github.workflow }}-${{ github.job }}-
|
|
|
|
- name: Enable ccache
|
|
run: |
|
|
set -eu
|
|
export CCACHE_DIR="$HOME/.ccache"
|
|
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
|
|
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
|
|
ccache -M 250M
|
|
ccache -z
|
|
|
|
- name: Build database_task
|
|
shell: bash
|
|
working-directory: samples/tasks/database
|
|
run: |
|
|
set -eu
|
|
make clean && make -j$(nproc) all
|
|
test -x database_task
|
|
|
|
- name: Smoke-test database_task argv parsing
|
|
shell: bash
|
|
working-directory: samples/tasks/database
|
|
run: |
|
|
set -u
|
|
# database_task with no args should print the usage line and
|
|
# exit non-zero. This catches build regressions in the argv
|
|
# parsing / main() entry path without depending on the task
|
|
# queue actually running to completion -- task_push_dbscan
|
|
# delegates to task_push_manual_content_scan which does not
|
|
# plumb through the caller's completion callback, so the
|
|
# loop can only be terminated externally (by timeout or
|
|
# signal). The full loop is exercised in local testing; in
|
|
# CI we settle for the argv smoke.
|
|
#
|
|
# We capture the exit status via "|| rc=$?" so that the
|
|
# expected non-zero exit does NOT trip the default "set -e"
|
|
# that GitHub Actions' bash shell injects (it wraps with
|
|
# "bash --noprofile --norc -eo pipefail"). Without this
|
|
# guard the step exits at the binary invocation before our
|
|
# assertion runs.
|
|
rc=0
|
|
./database_task > /dev/null 2>&1 || rc=$?
|
|
if [[ $rc -eq 0 ]]; then
|
|
echo "::error title=Test failed::database_task with no args exited 0; expected non-zero (usage)"
|
|
exit 1
|
|
fi
|
|
echo "[pass] database_task no-args exit=$rc (expected non-zero)"
|
|
|
|
- name: Build and run archive_name_safety_test, decompress_window_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/decompress
|
|
run: |
|
|
set -eu
|
|
make clean && make -j$(nproc) all
|
|
test -x archive_name_safety_test
|
|
# Regression test for the Zip Slip / absolute-path defences
|
|
# in tasks/task_decompress.c::archive_name_is_safe(). The
|
|
# test keeps a verbatim copy of the predicate and runs it
|
|
# against 23 safe and unsafe inputs. If task_decompress.c
|
|
# ever amends the predicate, the copy here must follow.
|
|
timeout 60 ./archive_name_safety_test
|
|
echo "[pass] archive_name_safety_test"
|
|
# Oracle for the extraction task's pacing, compiled from the
|
|
# shipping translation unit against the real unthreaded
|
|
# task queue, ZIP backend and shared per-frame I/O window,
|
|
# with a virtual clock: a whole archive fits one window, an
|
|
# exhausted window still extracts one item per call. A
|
|
# DEFLATE member larger than the backend's output window is
|
|
# written a window at a time, through a temporary file that
|
|
# a CRC failure or a cancel removes, leaving any previous
|
|
# file in place.
|
|
timeout 60 ./decompress_window_test
|
|
make clean >/dev/null
|
|
# The same lanes against the built-in inflater, which builds
|
|
# without HAVE_ZLIB use and which rebinds its output window
|
|
# differently from zlib.
|
|
make -j$(nproc) all BUILTIN_DEFLATE=1
|
|
timeout 60 ./decompress_window_test
|
|
make clean BUILTIN_DEFLATE=1 >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 120 ./decompress_window_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./decompress_window_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run cloudsync_path_safety_test
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
# Regression test for the manifest-key check in
|
|
# tasks/task_cloudsync.c::task_cloud_sync_fetch_server_file.
|
|
# A hostile sync server's manifest key could escape the
|
|
# cloud-sync directory via ".." or an absolute component, or
|
|
# crash the fetch with a key that has no '/'. The test
|
|
# #includes the real predicate (tasks/task_cloudsync_path.c).
|
|
make clean && make -j$(nproc) all
|
|
test -x cloudsync_path_safety_test
|
|
timeout 60 ./cloudsync_path_safety_test
|
|
echo "[pass] cloudsync_path_safety_test"
|
|
|
|
- name: Build and run cloudsync_manifest_adds_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
# tasks/task_cloudsync.c's updated manifests, added to from
|
|
# four threads and the task's own at once with no lock: each
|
|
# entry is pushed on a list the task thread folds in. Every
|
|
# entry must be there once, with its own hash; TSan is what
|
|
# sees the threads share anything without ordering, ASan an
|
|
# entry freed twice or not at all.
|
|
make clean && make -j$(nproc) cloudsync_manifest_adds_test
|
|
timeout 60 ./cloudsync_manifest_adds_test
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) cloudsync_manifest_adds_test SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 120 ./cloudsync_manifest_adds_test
|
|
make clean SANITIZER=thread && make -j$(nproc) cloudsync_manifest_adds_test SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./cloudsync_manifest_adds_test
|
|
make clean >/dev/null
|
|
echo "[pass] cloudsync_manifest_adds_test"
|
|
|
|
- name: Build and run cloudsync_pacing_test (ASan + UBSan + LSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
# tasks/task_cloudsync.c's diff runs as many steps as the shared
|
|
# per-frame I/O window grants and hashes the current file 256 KiB
|
|
# at a time, carrying a large file over to the next run instead
|
|
# of reading it whole in one (the frame loop, with Threaded Tasks
|
|
# off). Against a stub window: a 9 MiB file spread over runs with
|
|
# its MD5 unchanged, small files several to a run, the floor
|
|
# alone finishing the diff, and a 9 MiB download reported from
|
|
# another thread hashed by the task over many runs instead of
|
|
# by the callback. The directory walk listing the local files
|
|
# runs within the grant too and lists what dir_list_new() did,
|
|
# an unreadable root still stopping the sync. Pushed on the real
|
|
# task queue and run to its
|
|
# end, the sync must release everything (LeakSanitizer).
|
|
make clean && make -j$(nproc) cloudsync_pacing_test
|
|
timeout 120 ./cloudsync_pacing_test
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) cloudsync_pacing_test SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 300 ./cloudsync_pacing_test
|
|
make clean SANITIZER=thread && make -j$(nproc) cloudsync_pacing_test SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./cloudsync_pacing_test
|
|
make clean >/dev/null
|
|
echo "[pass] cloudsync_pacing_test"
|
|
|
|
- name: Build and run the cloudsync hand-over tests, locked path (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
# Completion callbacks hand manifest entries and downloads to
|
|
# the task thread on lock-free lists where the atomics backend
|
|
# has pointer operations, and under tcs_manifest_lock where it
|
|
# does not. Every backend this runner has, has them, so
|
|
# FORCE_LOCK=1 builds tasks/task_cloudsync.c with
|
|
# TASK_CLOUDSYNC_FORCE_LOCK: the locked path on the host's own
|
|
# backend, where TSan sees an append or hand-over the lock
|
|
# does not cover. Same tests, same checks, other path.
|
|
for t in cloudsync_manifest_adds_test cloudsync_pacing_test cloudsync_commit_test; do
|
|
make clean $t FORCE_LOCK=1
|
|
timeout 120 ./$t
|
|
make clean $t FORCE_LOCK=1 SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 300 ./$t
|
|
make clean $t FORCE_LOCK=1 SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./$t
|
|
done
|
|
make clean >/dev/null
|
|
echo "[pass] cloudsync hand-over, locked path"
|
|
|
|
- name: Check cloud-sync commits on the content thread
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) cloudsync_commit_test SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 ./cloudsync_commit_test
|
|
make clean SANITIZER=thread && make -j$(nproc) cloudsync_commit_test SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 ./cloudsync_commit_test
|
|
make clean CC=x86_64-w64-mingw32-gcc LDFLAGS=-static && make -j$(nproc) cloudsync_commit_test CC=x86_64-w64-mingw32-gcc LDFLAGS=-static
|
|
WINEDEBUG=-all wine ./cloudsync_commit_test.exe
|
|
make clean
|
|
|
|
- name: Build and run cloudsync_async_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
# network/cloud_sync_driver.c runs a blocking driver's calls
|
|
# (SMB, NFS) on a worker and hands results back through
|
|
# cloud_sync_poll(), so a slow server never stalls the thread
|
|
# driving the sync - the frame loop with Threaded Tasks off.
|
|
# Against a driver that blocks until released: every call
|
|
# returns at once, handlers run on the polling thread in call
|
|
# order, one driver call at a time, names copied, refused calls
|
|
# failing, and a new worker for the sync after one that ended
|
|
# or failed to begin. On the old direct calls the test hangs.
|
|
make clean && make -j$(nproc) cloudsync_async_test
|
|
timeout 60 ./cloudsync_async_test
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) cloudsync_async_test SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 120 ./cloudsync_async_test
|
|
make clean SANITIZER=thread && make -j$(nproc) cloudsync_async_test SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./cloudsync_async_test
|
|
make clean >/dev/null
|
|
echo "[pass] cloudsync_async_test"
|
|
|
|
- name: Build and run natt_desc_parse_test
|
|
shell: bash
|
|
working-directory: samples/tasks/natt_desc
|
|
run: |
|
|
set -eu
|
|
# Regression test for the UPnP IGD description parser
|
|
# (network/natt_desc.c::natt_parse_desc_node). A router's
|
|
# description XML is untrusted; the old parser crashed on a
|
|
# leaf or empty root node and never reached a nested
|
|
# <service>. The test #includes the real parser and feeds it
|
|
# hand-built hostile trees.
|
|
make clean && make -j$(nproc) all
|
|
test -x natt_desc_parse_test
|
|
timeout 60 ./natt_desc_parse_test
|
|
echo "[pass] natt_desc_parse_test"
|
|
|
|
- name: Build and run http_method_match_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# Regression test for the heap-buffer-overflow fix in
|
|
# tasks/task_http.c::task_push_http_transfer_generic().
|
|
# Pre-fix, an unsafe memcmp(method, "GET", 3) read past
|
|
# the strdup'd buffer when method was NULL or shorter
|
|
# than 3 bytes. Build under AddressSanitizer so a
|
|
# regression to the unsafe expression is caught at the
|
|
# bounds level, not just the truth-table level. If
|
|
# task_http.c amends the GET-dispatch predicate, the
|
|
# copy in http_method_match_test.c must follow.
|
|
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
|
|
test -x http_method_match_test
|
|
timeout 60 ./http_method_match_test
|
|
echo "[pass] http_method_match_test"
|
|
|
|
- name: Build and run video_shader_wildcard_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/video_shader_parse
|
|
run: |
|
|
set -eu
|
|
# Regression test for the buffer-overflow defences in
|
|
# gfx/video_shader_parse.c::video_shader_replace_wildcards_impl().
|
|
# Pre-fix, _len = strlcpy(replace_text, source, 256)
|
|
# returned strlen(source) regardless of the 256-byte
|
|
# cap. When source was a $CONTENT-DIR$ / $PRESET-DIR$
|
|
# / $CORE$ value longer than 256 bytes (DIR_MAX_LENGTH
|
|
# is 4096, NAME_MAX_LENGTH 256), the subsequent
|
|
# memcpy(dst + prefix, replace_text, _len) read off
|
|
# the end of replace_text and the strlcpy's size
|
|
# argument PATH_MAX_LENGTH - prefix - _len underflowed
|
|
# size_t. Build under AddressSanitizer so the OOB
|
|
# read on replace_text and the underflow-driven OOB
|
|
# write on dst are both caught at the bounds level.
|
|
# If video_shader_parse.c amends the post-replacement
|
|
# arithmetic, the verbatim copy in
|
|
# video_shader_wildcard_test.c must follow.
|
|
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
|
|
test -x video_shader_wildcard_test
|
|
timeout 60 ./video_shader_wildcard_test
|
|
echo "[pass] video_shader_wildcard_test"
|
|
|
|
- name: Build and run cloudsync_savefile_owner_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/cloudsync
|
|
run: |
|
|
set -eu
|
|
# Regression test for the save-data rollback cloud sync could
|
|
# cause at startup. A core holds its save RAM in memory and
|
|
# writes it back when content closes, so a sync that replaces
|
|
# the file in between has its work overwritten and the stale
|
|
# result uploaded over the server's copy.
|
|
# save.c::content_savefile_is_live() is the ownership check
|
|
# tasks/task_cloudsync.c consults before it touches a local
|
|
# file; the test pins that predicate and the manifest key
|
|
# lookup the carry-forward uses to recover the hash the last
|
|
# sync recorded. Build under AddressSanitizer so a regression
|
|
# in the list walk is caught at the bounds level. If save.c
|
|
# amends the predicate, the verbatim copy in
|
|
# cloudsync_savefile_owner_test.c must follow.
|
|
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
|
|
test -x cloudsync_savefile_owner_test
|
|
timeout 60 ./cloudsync_savefile_owner_test
|
|
echo "[pass] cloudsync_savefile_owner_test"
|
|
|
|
- name: Build and run input_remap_bounds_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/input_remap
|
|
run: |
|
|
set -eu
|
|
# Regression test for the input-remap analog-axis OOB-
|
|
# write fixes in configuration.c::input_remapping_load_file()
|
|
# and input/input_driver.c. Pre-fix a malformed .rmp
|
|
# could supply a remap target outside [0, RARCH_ANALOG_
|
|
# BIND_LIST_END), which the use sites in input_driver.c
|
|
# then indexed into a fixed-size analog_value[][8] array
|
|
# without bounds (button -> analog branch) or with a
|
|
# broken bound that compared elements against bytes
|
|
# (analog -> analog branch using sizeof instead of
|
|
# ARRAY_SIZE). Build under AddressSanitizer so any
|
|
# reintroduction of either primitive is caught at the
|
|
# bounds level. If configuration.c or input_driver.c
|
|
# amends the relevant predicates, the verbatim copies
|
|
# in input_remap_bounds_test.c must follow.
|
|
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
|
|
test -x input_remap_bounds_test
|
|
timeout 60 ./input_remap_bounds_test
|
|
echo "[pass] input_remap_bounds_test"
|
|
|
|
- name: Build and run turbo_bind_bounds_test (ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/turbo_bind
|
|
run: |
|
|
set -eu
|
|
# Turbo Bind and Turbo Button hold a RetroPad ID that indexes
|
|
# input_config_bind_order[24] in the menu's left/right
|
|
# handlers and input_remap_ids[port][] in the turbo paths of
|
|
# input_driver.c. The test walks both settings in both
|
|
# directions with wrap-around on and off, feeds them values
|
|
# from outside their range, checks the load-time clamp in
|
|
# configuration.c and the empty-bind guard on the turbo
|
|
# clear, with the tables at their exact sizes so either
|
|
# sanitizer reports an index past an end. If
|
|
# menu_setting.c, configuration.c or input_driver.c change
|
|
# those functions, the copies in turbo_bind_bounds_test.c
|
|
# must follow.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
test -x turbo_bind_bounds_test
|
|
UBSAN_OPTIONS=halt_on_error=1 timeout 60 ./turbo_bind_bounds_test
|
|
echo "[pass] turbo_bind_bounds_test"
|
|
|
|
- name: Build and run bsv_replay_bounds_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/bsv_replay
|
|
run: |
|
|
set -eu
|
|
# Regression test for the .bsv replay-file event-count
|
|
# bounds in input/bsv/bsvmovie.c. Pre-fix a malformed
|
|
# replay file with key_event_count > 128 or
|
|
# input_event_count > 512 caused a heap-buffer-overflow
|
|
# write into bsv_movie_t (a calloc'd struct) with
|
|
# attacker-chosen 8/12-byte payloads at attacker-chosen
|
|
# offsets up to ~500KB. Build under AddressSanitizer
|
|
# so any reintroduction of the unbounded count is caught
|
|
# at the bounds level. If input/bsv/bsvmovie.c amends
|
|
# the predicate, the verbatim copy in
|
|
# bsv_replay_bounds_test.c must follow.
|
|
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
|
|
test -x bsv_replay_bounds_test
|
|
timeout 60 ./bsv_replay_bounds_test
|
|
echo "[pass] bsv_replay_bounds_test"
|
|
|
|
- name: Build and run bsv_checkpoint_bounds_test (ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/bsv_checkpoint
|
|
run: |
|
|
set -eu
|
|
# A checkpoint in a .bsv replay carries its state, encoded
|
|
# and compressed sizes, all from the file. The test feeds
|
|
# bsv_movie_load_checkpoint() checkpoints whose sizes
|
|
# disagree, truncated ones and skipped ones, with buffers at
|
|
# their exact sizes, so an overflow, a double free or a
|
|
# leak is reported. If input/bsv/bsvmovie.c changes that
|
|
# function, the verbatim copy in
|
|
# bsv_checkpoint_bounds_test.c must follow.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
test -x bsv_checkpoint_bounds_test
|
|
UBSAN_OPTIONS=halt_on_error=1 timeout 60 ./bsv_checkpoint_bounds_test
|
|
echo "[pass] bsv_checkpoint_bounds_test"
|
|
|
|
- name: Build and run bsv_replay_init_test (plain, ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/bsv_replay_init
|
|
run: |
|
|
set -eu
|
|
# Regression oracle for replay start-up and shutdown in
|
|
# tasks/task_movie.c and input/bsv/bsvmovie.c (#19622).
|
|
# Links the shipping units against a stub core and the real
|
|
# task queue / intfstream / VFS, in the iOS configuration
|
|
# (RZSTD on, STATESTREAM off). Pre-fix a replay truncated
|
|
# inside its first checkpoint, or halted before its first
|
|
# frame, left BSV_FLAG_MOVIE_END set with no handle: the
|
|
# runloop then paused after every frame, for every content,
|
|
# until the process was restarted. Also covers halting a
|
|
# recording before its first frame leaving the session stuck
|
|
# "recording". If runloop.c changes its per-frame replay
|
|
# protocol, frame() in the test must follow.
|
|
make clean && make -j$(nproc) all
|
|
test -x bsv_replay_init_test
|
|
timeout 120 ./bsv_replay_init_test
|
|
echo "[pass] bsv_replay_init_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
# allocator_may_return_null: the test feeds a checkpoint too big
|
|
# to allocate and expects malloc to return NULL.
|
|
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 UBSAN_OPTIONS=print_stacktrace=1 \
|
|
timeout 120 ./bsv_replay_init_test
|
|
echo "[pass] bsv_replay_init_test (ASan + UBSan)"
|
|
# STATESTREAM=1: the deduplicating checkpoint encoding desktop
|
|
# builds use, and the lanes that exercise it.
|
|
make clean STATESTREAM=1 SANITIZER=address,undefined && make -j$(nproc) all STATESTREAM=1 SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 UBSAN_OPTIONS=print_stacktrace=1 \
|
|
timeout 120 ./bsv_replay_init_test
|
|
echo "[pass] bsv_replay_init_test (STATESTREAM, ASan + UBSan)"
|
|
|
|
- name: Build and run netplay BSV recording tests
|
|
shell: bash
|
|
working-directory: samples/tasks/bsv_replay_init
|
|
run: |
|
|
set -eu
|
|
. "$GITHUB_WORKSPACE/.github/workflows/scripts/wine-server.sh"
|
|
make clean && make -j$(nproc) NETPLAY=1 SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
|
|
timeout 120 ./bsv_replay_init_test
|
|
make clean && make -j$(nproc) NETPLAY=1 STATESTREAM=1 SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
|
|
timeout 120 ./bsv_replay_init_test
|
|
make clean && make -j$(nproc) NETPLAY=1 CC=x86_64-w64-mingw32-gcc LDFLAGS=-static
|
|
WINEDEBUG=-all timeout 120 wine ./bsv_replay_init_test.exe
|
|
|
|
- name: Build and run bsv_statestream_test (plain, ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/bsv_statestream
|
|
run: |
|
|
set -eu
|
|
# Statestream checkpoints decoded by the shipping
|
|
# input/bsv/bsvmovie.c with HAVE_STATESTREAM on, which no
|
|
# other lane builds. Block and superblock indices, the
|
|
# superblock sequence and its length all come from the
|
|
# replay file: the test round-trips recorded checkpoints,
|
|
# then feeds sequences naming undefined superblocks and
|
|
# blocks, a sequence longer than the superblock list, a
|
|
# recording after a short sequence, and a header with no
|
|
# block layout. The oom and oomload lanes fail each
|
|
# allocation of a checkpoint write and of a load in turn
|
|
# (malloc, calloc and realloc are wrapped at link time) and
|
|
# need every one to fail cleanly, with no leak. Each lane
|
|
# takes its name as an argument to run alone.
|
|
make clean && make -j$(nproc) all
|
|
test -x bsv_statestream_test
|
|
timeout 120 ./bsv_statestream_test
|
|
echo "[pass] bsv_statestream_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
|
|
timeout 120 ./bsv_statestream_test
|
|
echo "[pass] bsv_statestream_test (ASan + UBSan)"
|
|
# Again with every object hashing alike (constant_hash.h),
|
|
# so inserts share buckets and the oom lanes also fail the
|
|
# buckets' own growth.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined \
|
|
EXTRA_CFLAGS='-include $(CURDIR)/constant_hash.h'
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
|
|
timeout 120 ./bsv_statestream_test
|
|
echo "[pass] bsv_statestream_test (colliding hash)"
|
|
|
|
- name: Build and run bps_patch_bounds_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/bps_patch
|
|
run: |
|
|
set -eu
|
|
# Regression test for the .bps patch parser bounds in
|
|
# tasks/task_patch.c::bps_apply_patch. Pre-fix a
|
|
# malicious .bps could write attacker-chosen bytes
|
|
# past the malloc'd target_data buffer (heap-buffer-
|
|
# overflow WRITE), read past source_data (info leak),
|
|
# and read past modify_data via TARGET_READ. The
|
|
# size-prelude was also unbounded, allowing 32-bit
|
|
# truncation of modify_target_size to drive a
|
|
# smaller-than-expected allocation. Build under
|
|
# AddressSanitizer so any reintroduction is caught
|
|
# at the bounds level. If task_patch.c amends the
|
|
# action-loop predicates, the verbatim copy in
|
|
# bps_patch_bounds_test.c must follow.
|
|
make clean SANITIZER=address && make -j$(nproc) all SANITIZER=address
|
|
test -x bps_patch_bounds_test
|
|
timeout 60 ./bps_patch_bounds_test
|
|
echo "[pass] bps_patch_bounds_test"
|
|
|
|
- name: Build and run patch_stream_test (ASan/UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/patch_stream
|
|
run: |
|
|
set -eu
|
|
# Differential harness for the streaming patch appliers in
|
|
# tasks/patch_stream.c. Each streamed applier must produce
|
|
# output byte-identical to the whole-buffer applier it
|
|
# mirrors in tasks/task_patch.c, at every chunk size - the
|
|
# oracle for IPS, UPS and BPS is a copy of that whole-buffer
|
|
# logic held in the test, so if task_patch.c changes its
|
|
# appliers the copy has to follow. The .xdelta case checks
|
|
# the delegation to the VCDIFF decoder instead, since that
|
|
# decoder is verified separately against the reference
|
|
# implementation.
|
|
#
|
|
# The cases that matter most are the ones a whole-buffer
|
|
# applier cannot have: an empty source, a source that ends
|
|
# short of its declared length, and chunk boundaries landing
|
|
# inside a patch command. A truncated feed must be refused
|
|
# rather than answered with a target built from a partial
|
|
# source.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
test -x patch_stream_test
|
|
timeout 900 ./patch_stream_test
|
|
echo "[pass] patch_stream_test"
|
|
|
|
- name: Build and run task_file_transfer_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/file_transfer
|
|
run: |
|
|
set -eu
|
|
# Oracle for tasks/task_file_transfer.c, compiled from the
|
|
# tree so it exercises the shipping translation unit. The
|
|
# handler is a switch whose states advance at most one step
|
|
# per tick, so what matters is how many ticks a load takes,
|
|
# whether every input reaches a terminal at all, and what a
|
|
# caller reading task_get_progress() sees on the way - a
|
|
# test that only checks the bytes passes on every defect
|
|
# below. Lanes run to a tick cap rather than looping, so a
|
|
# task that never finishes reports instead of hanging CI.
|
|
#
|
|
# Pinned here: a NULL path and a parse callback that
|
|
# returns success without setting is_finished, both of
|
|
# which once left the task running forever with no flag set
|
|
# (a per-frame call under the regular queue, a hot spin on
|
|
# the worker thread under the threaded one); progress
|
|
# reaching 100 on every completion path, not just the
|
|
# single-tick one; the tick that opens the file also
|
|
# reading, which one byte either side of the threshold used
|
|
# to decide; a file truncated underneath an in-flight fill
|
|
# cancelling rather than parsing a buffer whose tail was
|
|
# never written; and every audio type reaching the mixer
|
|
# arm, since M4A and OPUS were once absent from that switch
|
|
# while both push paths set them, so those sounds loaded
|
|
# and silently did not play.
|
|
#
|
|
# Two lanes guard the 256 MiB prefix cap that cancelled
|
|
# every large non-faststart video. The real-demuxer lane
|
|
# pins the premise the cap got wrong: a genuine h264 MP4
|
|
# (real ftyp/mdat/moov, real coded frames) is written with
|
|
# a ~300 MiB free box pushing moov to EOF - a valid file,
|
|
# the layout most cameras produce - and the real rmp4, via
|
|
# image_transfer's avail-aware open, must answer need_more
|
|
# on a 1 MiB prefix and decode real frames once fully
|
|
# resident. Anyone restoring a prefix ceiling on the
|
|
# "a still only needs a few per cent" argument is refuted
|
|
# by that lane directly. The task lane pins the
|
|
# mechanism: a still ready only at EOF, over a 300 MiB
|
|
# file, must finish uncancelled with every byte resident.
|
|
# On the capped tree it stops at exactly 268435456 and
|
|
# cancels - no thumbnail, no animation - and the oracle
|
|
# exits non-zero.
|
|
#
|
|
# The conc lane runs disjoint handles on many threads at
|
|
# once. data_transfer synchronises nothing by design and
|
|
# the task layer gives one handle to one task, so it is not
|
|
# looking for contention on a shared handle - it checks
|
|
# that the spine keeps no hidden static state, which a
|
|
# sanitizer build can answer and inspection cannot.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
test -x task_file_transfer_test
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./task_file_transfer_test
|
|
ASAN_OPTIONS=detect_leaks=1 timeout 600 \
|
|
./task_file_transfer_test conc
|
|
echo "[pass] task_file_transfer_test (ASan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./task_file_transfer_test
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./task_file_transfer_test conc
|
|
echo "[pass] task_file_transfer_test (TSan)"
|
|
|
|
- name: Build and run pl_thumbnail_wait_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/pl_thumbnail
|
|
run: |
|
|
set -eu
|
|
# The thumbnail tasks park while a transfer runs, and a
|
|
# cancelled one keeps its handle until the transfer's
|
|
# callback has written into it.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
|
|
timeout 120 ./pl_thumbnail_wait_test
|
|
echo "[pass] pl_thumbnail_wait_test (ASan + UBSan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./pl_thumbnail_wait_test
|
|
echo "[pass] pl_thumbnail_wait_test (TSan)"
|
|
|
|
- name: Build and run core_crc_slice_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_updater
|
|
run: |
|
|
set -eu
|
|
# Oracle for the resumable CRC in
|
|
# libretro-common/streams/interface_stream.c and the sliced
|
|
# hashing loop tasks/task_core_updater.c drives it with.
|
|
#
|
|
# intfstream_get_crc() consumes a whole stream in one call,
|
|
# so the cost of the tick that invoked it was a function of
|
|
# core size and nothing else - 43ms for a 40MB core measured
|
|
# cold on NVMe, an order of magnitude worse on the SD-card
|
|
# and spinning-disk targets, and the updater hashes the
|
|
# installed core on every update check. intfstream_crc_step()
|
|
# hashes a bounded slice and resumes, so the work spreads
|
|
# across ticks.
|
|
#
|
|
# The property that needs pinning is not "the CRC is
|
|
# correct" - a whole-stream call gets that right too - but
|
|
# that no single tick hashes the whole file while the answer
|
|
# still matches the unsliced value at every chunk size,
|
|
# including sizes that do not divide the file evenly and a
|
|
# final slice of one byte. Both are asserted against
|
|
# intfstream_get_crc() as the oracle.
|
|
make clean && make -j$(nproc) all
|
|
test -x core_crc_slice_test
|
|
timeout 600 ./core_crc_slice_test
|
|
echo "[pass] core_crc_slice_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./core_crc_slice_test
|
|
echo "[pass] core_crc_slice_test (ASan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 ./core_crc_slice_test
|
|
make clean
|
|
echo "[pass] core_crc_slice_test (TSan)"
|
|
|
|
- name: Build and run get_list_refresh_flags_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_updater
|
|
run: |
|
|
set -eu
|
|
# Regression lane for the menu refresh flags around the
|
|
# core updater get-list task, against the shipping
|
|
# tasks/task_core_updater.c, tasks/task_http.c,
|
|
# task_queue.c and net_http.c.
|
|
#
|
|
# The Core Downloader entry point sets
|
|
# MENU_ST_FLAG_ENTRIES_NONBLOCKING_REFRESH before pushing
|
|
# the get-list task, and only the task's retirement
|
|
# callback clears it again. The callback reads
|
|
# refresh_menu through task->state, so the handle has to
|
|
# stay attached to the task until retrieval; a handler
|
|
# that frees it early turns the clear into a silent no-op,
|
|
# MENU_ENTRIES_NEEDS_REFRESH() then stays false for the
|
|
# rest of the session, and one visit to Core Downloader
|
|
# wedges every deeper menu until restart. The test drives
|
|
# the real push end to end against a loopback
|
|
# .index-extended server, both values of refresh_menu, and
|
|
# asserts the flags clear and the refresh gate comes back.
|
|
make -f Makefile.refresh_flags clean && make -f Makefile.refresh_flags -j$(nproc) all
|
|
test -x get_list_refresh_flags_test
|
|
timeout 600 ./get_list_refresh_flags_test
|
|
echo "[pass] get_list_refresh_flags_test"
|
|
make -f Makefile.refresh_flags clean SANITIZER=address,undefined && make -f Makefile.refresh_flags -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./get_list_refresh_flags_test
|
|
echo "[pass] get_list_refresh_flags_test (ASan)"
|
|
make -f Makefile.refresh_flags clean SANITIZER=thread && make -f Makefile.refresh_flags -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./get_list_refresh_flags_test
|
|
make -f Makefile.refresh_flags clean
|
|
echo "[pass] get_list_refresh_flags_test (TSan)"
|
|
|
|
- name: Build and run update_installed_scan_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_updater
|
|
run: |
|
|
set -eu
|
|
# Regression lane for the Update Installed Cores scan in
|
|
# tasks/task_core_updater.c, against the shipping task, list
|
|
# parser, HTTP task and task queue. Every list entry and
|
|
# every CRC chunk is a work item of the shared per-frame I/O
|
|
# window (task_nbio_slice_*), so with the unthreaded queue a
|
|
# 512-entry list finishes in a few dozen frames rather than
|
|
# one frame per entry; a CRC resumed on a later tick does not
|
|
# repeat the per-core stats. Both are asserted: the gather
|
|
# count, and path_is_valid() calls counted at the link
|
|
# boundary. The get-list lane forces the window to its
|
|
# one-item floor and asserts the listing is parsed a line
|
|
# per gather into a private list, while the caller's list -
|
|
# read by the Core Downloader's menu callbacks every frame -
|
|
# only ever holds its old contents or the complete new ones.
|
|
# The scan lanes also count core info reads at the link
|
|
# boundary: Update Installed Cores' fetch reads info for its
|
|
# installed cores alone. The status lanes pin that a fetch
|
|
# finding no installed cores still ends in 'all cores
|
|
# updated' and a failed fetch in 'core list failed'.
|
|
# The refused-download lane refuses a changed core's transfer
|
|
# and asserts the run still completes: a download that
|
|
# cannot start fails instead of waiting on an extraction.
|
|
# The CRC cache lane counts bytes hashed at the link
|
|
# boundary: a repeat run hashes no unchanged core, a touched
|
|
# core is hashed again, a changed one is hashed and
|
|
# downloaded, and a corrupt cache falls back to hashing all.
|
|
make -f Makefile.update_installed clean && make -f Makefile.update_installed -j$(nproc) all
|
|
test -x update_installed_scan_test
|
|
timeout 600 ./update_installed_scan_test
|
|
echo "[pass] update_installed_scan_test"
|
|
make -f Makefile.update_installed clean SANITIZER=address,undefined && make -f Makefile.update_installed -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./update_installed_scan_test
|
|
echo "[pass] update_installed_scan_test (ASan)"
|
|
make -f Makefile.update_installed clean SANITIZER=thread && make -f Makefile.update_installed -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./update_installed_scan_test
|
|
make -f Makefile.update_installed clean
|
|
echo "[pass] update_installed_scan_test (TSan)"
|
|
|
|
- name: Build and run staged_install_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_updater
|
|
run: |
|
|
set -eu
|
|
# End-to-end lane for the staged core install against the
|
|
# shipping updater, HTTP, decompress, archive, rzip and core
|
|
# backup code, serving real deflated zips: with 'Compress
|
|
# Automatic Core Backups' off the replaced core is moved into
|
|
# the backups as it is; the history is pruned; a move refused
|
|
# at the link boundary falls back to a compressed copy; with
|
|
# the setting on the backup is compressed as before; a new
|
|
# core that cannot be moved into place leaves the installed
|
|
# one as it was.
|
|
make -f Makefile.staged_install clean && make -f Makefile.staged_install -j$(nproc) all
|
|
timeout 600 ./staged_install_test
|
|
echo "[pass] staged_install_test"
|
|
make -f Makefile.staged_install clean SANITIZER=address,undefined && make -f Makefile.staged_install -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./staged_install_test
|
|
echo "[pass] staged_install_test (ASan)"
|
|
make -f Makefile.staged_install clean SANITIZER=thread && make -f Makefile.staged_install -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./staged_install_test
|
|
make -f Makefile.staged_install clean
|
|
echo "[pass] staged_install_test (TSan)"
|
|
|
|
- name: Build and run download_backup_wait_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_updater
|
|
run: |
|
|
set -eu
|
|
# Regression lane for the core download task's wait on its
|
|
# automatic backup in tasks/task_core_updater.c. The threaded
|
|
# queue frees a retired backup task on the main thread while
|
|
# the download handler runs on the worker, so completion has
|
|
# to come from the backup's callback rather than from reading
|
|
# the task. One lane retires and frees the backup between two
|
|
# download ticks (a handler that reads it is a
|
|
# heap-use-after-free under ASan); the other cancels the
|
|
# download while the backup runs, so the backup's callback
|
|
# fires after the download's handle is gone.
|
|
make -f Makefile.download_backup clean && make -f Makefile.download_backup -j$(nproc) all
|
|
test -x download_backup_wait_test
|
|
timeout 600 ./download_backup_wait_test
|
|
echo "[pass] download_backup_wait_test"
|
|
make -f Makefile.download_backup clean SANITIZER=address,undefined && make -f Makefile.download_backup -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./download_backup_wait_test
|
|
echo "[pass] download_backup_wait_test (ASan)"
|
|
make -f Makefile.download_backup clean SANITIZER=thread && make -f Makefile.download_backup -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./download_backup_wait_test
|
|
make -f Makefile.download_backup clean
|
|
echo "[pass] download_backup_wait_test (TSan)"
|
|
|
|
- name: Build and run pfd_switch_cores_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_updater
|
|
run: |
|
|
set -eu
|
|
# Regression lane for the Play Feature Delivery 'switch
|
|
# installed cores' task in tasks/task_core_updater.c, built
|
|
# with ANDROID defined against stand-ins for the five
|
|
# play_feature_delivery_* calls. The switch task learns of
|
|
# each install's completion and error from the install's
|
|
# callback rather than by reading the task, which the queue
|
|
# frees as it retires it; with the threaded queue, a wait
|
|
# that reads the task moves on before the install is retired
|
|
# and the next push is refused, skipping cores. The other
|
|
# lanes end the switch on a failed install, and cancel the
|
|
# switch while an install runs. The recovery lanes leave
|
|
# the <core>.bak an interrupted install moves the core to,
|
|
# with the core missing: the next install must put it back
|
|
# if it fails and drop it if it succeeds or play feature
|
|
# delivery already installed the core.
|
|
make -f Makefile.pfd_switch clean && make -f Makefile.pfd_switch -j$(nproc) all
|
|
test -x pfd_switch_cores_test
|
|
timeout 600 ./pfd_switch_cores_test
|
|
echo "[pass] pfd_switch_cores_test"
|
|
make -f Makefile.pfd_switch clean SANITIZER=address,undefined && make -f Makefile.pfd_switch -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./pfd_switch_cores_test
|
|
echo "[pass] pfd_switch_cores_test (ASan)"
|
|
make -f Makefile.pfd_switch clean SANITIZER=thread && make -f Makefile.pfd_switch -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./pfd_switch_cores_test
|
|
make -f Makefile.pfd_switch clean
|
|
echo "[pass] pfd_switch_cores_test (TSan)"
|
|
|
|
- name: Build and run core_backup_io_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/core_backup
|
|
run: |
|
|
set -eu
|
|
# Oracle for the file I/O in tasks/task_core_backup.c,
|
|
# compiled from the tree so it exercises the shipping
|
|
# translation unit. As in save_state and
|
|
# image_decode_slice, the clock is the test's instrument:
|
|
# cpu_features_get_time_usec is wrapped so the tick budget is
|
|
# assertable exactly rather than measured against a wall
|
|
# clock.
|
|
#
|
|
# The throughput lanes pin the budget that replaced a fixed
|
|
# one-quantum-per-tick loop: a 4KB quantum capped a backup at
|
|
# ~245KB/s whatever the device could do, and an automatic
|
|
# backup runs on every core update over cores that are
|
|
# routinely tens of megabytes. The exhausted-budget lane
|
|
# asserts the floor from the other side - with a clock that
|
|
# spends the budget on its first observation the handler must
|
|
# still move exactly one quantum per tick and still produce a
|
|
# correct backup, which is the old behaviour reached through
|
|
# the new code and what the slowest supported storage gets.
|
|
#
|
|
# The read-amplification lane counts bytes at the VFS
|
|
# boundary, which is the only place they can be counted:
|
|
# interposing libc read()/write() silently reports zero here,
|
|
# because glibc's stdio reaches them through internal aliases
|
|
# that never go via the PLT. It measures 2x when the CRC is
|
|
# unknown - the handler hashes the core to name the backup,
|
|
# then reads it again to copy it - and 1x when the caller
|
|
# supplies a CRC. The second figure is the one that matters:
|
|
# tasks/task_core_updater.c hands its already-computed
|
|
# local_crc to the automatic backup precisely so that pass is
|
|
# skipped, and nothing in the type system enforces it. Pass
|
|
# 0 from the updater and every automatic backup silently
|
|
# doubles its read I/O.
|
|
#
|
|
# The restore lane round-trips a backup byte for byte. It is
|
|
# worth noting why it is asserted rather than skipped: it
|
|
# used to skip, silently, on every run. The suite stubbed
|
|
# frontend_driver_get_core_extension() with the wrong
|
|
# signature - const char *f(void) against the real
|
|
# bool f(char *, size_t) - which compiles, because nothing
|
|
# includes the real declaration, but left core_backup.c
|
|
# comparing an extension against uninitialised stack. Every
|
|
# archive read as CORE_BACKUP_TYPE_INVALID,
|
|
# task_push_core_restore() refused, and the lane printed SKIP
|
|
# while the suite reported PASSED. Both refusal paths are
|
|
# now CHECK failures: a fixture this test builds itself
|
|
# cannot legitimately be unavailable to it.
|
|
#
|
|
# The interrupted-restore lanes stop a restore one quantum
|
|
# in, by cancelling it (as the exit drain does) and by
|
|
# failing its writes (a full disk), and require the
|
|
# installed core to be byte-identical afterwards with no
|
|
# temporary left: the restore writes beside the core and
|
|
# renames over it only once complete. The symlink lane
|
|
# requires a core reached through a link to keep the link
|
|
# and have its target replaced.
|
|
make clean && make -j$(nproc) all
|
|
test -x core_backup_io_test
|
|
timeout 600 ./core_backup_io_test
|
|
echo "[pass] core_backup_io_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./core_backup_io_test
|
|
echo "[pass] core_backup_io_test (ASan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 ./core_backup_io_test
|
|
make clean
|
|
echo "[pass] core_backup_io_test (TSan)"
|
|
|
|
- name: Build and run image_decode_slice_test (ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/image_decode_slice
|
|
run: |
|
|
set -eu
|
|
# Oracle for the shared decode window in tasks/task_image.c,
|
|
# compiled from the tree so it exercises the shipping
|
|
# translation unit. The clock is the test's instrument: the
|
|
# sample provides cpu_features_get_time_usec() itself,
|
|
# advancing a fixed quantum per observation, so the decode
|
|
# loops are budget-bound deterministically and the cost of a
|
|
# gather is readable as the fake-clock delta across
|
|
# task_queue_check() - no wall-clock measurement, no
|
|
# flakiness.
|
|
#
|
|
# The regression it pins has two faces, both verified to
|
|
# fail it: the pre-window code, where eight concurrent
|
|
# decodes each took a full frame_duration per gather (~8
|
|
# frames of decode in one gather), and the subtler
|
|
# period == allowance variant of the window itself, where a
|
|
# task that spends its whole allowance has aged the window a
|
|
# full period and the next task in the same gather resets
|
|
# it - the multiplication surviving a fix that reads
|
|
# correct. That variant passed inspection and failed
|
|
# simulation, which is why this oracle asserts on measured
|
|
# gather cost rather than on the code's shape.
|
|
#
|
|
# It also checks the decode itself end to end - dimensions
|
|
# and a pixel spot-check on every delivered image - so the
|
|
# budget rework cannot silently break what it budgets.
|
|
make clean && make -j$(nproc) all
|
|
test -x image_decode_slice_test
|
|
timeout 600 ./image_decode_slice_test
|
|
echo "[pass] image_decode_slice_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./image_decode_slice_test
|
|
echo "[pass] image_decode_slice_test (ASan)"
|
|
|
|
- name: Build and run scan_begin_budget_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/database
|
|
run: |
|
|
set -eu
|
|
# Pacing + throughput oracle for the manual content scanner's
|
|
# BEGIN/END split in tasks/task_database.c, compiled from the
|
|
# tree so it exercises the shipping translation units.
|
|
#
|
|
# The regression it pins: MANUAL_SCAN_BEGIN used to perform
|
|
# the entire recursive directory walk, the whole DAT load and
|
|
# the playlist setup in one handler invocation, and
|
|
# MANUAL_SCAN_END flushed every accumulated result (each add
|
|
# a linear playlist_entry_exists probe) plus a sort and a
|
|
# write in another - 166 ms measured for the 5000-file
|
|
# fixture, a hard frame freeze whenever Threaded Tasks is
|
|
# off. Both are now sequences of sub-states under the shared
|
|
# per-frame I/O window (tasks/task_nbio_slice.c, the same
|
|
# window the file-transfer spine uses), with completed
|
|
# sub-states collapsing into one gather so small libraries
|
|
# keep single-tick latency.
|
|
#
|
|
# Three wall-clock assertions guard the plain build, because
|
|
# any one alone can be satisfied by a scanner that is worse:
|
|
# pacing (no gather over the window plus bounded slack),
|
|
# first feedback within two frames of the push, and total
|
|
# scan time within a factor of the blocking walk + DAT parse
|
|
# it replaced - a scanner that no longer freezes but takes
|
|
# twice as long fails the third lane. Playlist parity (every
|
|
# fixture file present exactly once) is asserted in every
|
|
# mode.
|
|
#
|
|
# The sanitizer runs use the 'sanitize' mode, which runs
|
|
# every lane but skips the wall-clock assertions a sanitized
|
|
# build cannot honestly meet. Those lanes are where the
|
|
# in-flight resources the split introduced - an open
|
|
# directory iterator, an open DAT stream over a half-filled
|
|
# buffer, an open flush playlist - are made to die at every
|
|
# phase: a 14-point cancellation sweep lands a cancel in
|
|
# setup, mid-walk, mid-DAT-read, content iteration and
|
|
# mid-flush, and LeakSanitizer decides whether the teardown
|
|
# released everything (it already found one 16-byte-per-scan
|
|
# leak that predates the split). The threaded lane runs the
|
|
# same scan under the threaded queue and is the TSan target:
|
|
# the shared window deliberately opts threaded queues out of
|
|
# its static state, and this is where a mistake in that
|
|
# opt-out would surface.
|
|
#
|
|
# Two dedup lanes guard the playlist path-hash index that
|
|
# replaced the flush's quadratic existence probes: a
|
|
# differential lane asserts the index answers exactly like
|
|
# the linear playlist_entry_exists scan over plain, bare-
|
|
# archive, inside-archive and case-variant paths with fuzzy
|
|
# archive matching both off and on, and a no-overwrite
|
|
# rescan lane re-scans the fixture into the populated
|
|
# playlist and asserts zero duplicates, driving the index's
|
|
# budgeted seeding at scale. The throughput factor is
|
|
# tightened to reject the pre-index quadratic flush.
|
|
#
|
|
# The big-DAT lane scans 50 files against a 300k-game DAT
|
|
# and asserts the same pacing budget: no gather may grow
|
|
# with DAT size now that the rxml parse and the search-
|
|
# index build step under the shared window. Its label
|
|
# check samples the end of the list, so an index that
|
|
# silently covered only a prefix fails correctness, and
|
|
# three extra cancel points land inside the budgeted read
|
|
# and parse of the large document.
|
|
make clean && make -j$(nproc) all
|
|
test -x scan_begin_budget_test
|
|
timeout 600 ./scan_begin_budget_test
|
|
echo "[pass] scan_begin_budget_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 900 \
|
|
./scan_begin_budget_test sanitize
|
|
echo "[pass] scan_begin_budget_test (ASan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 900 \
|
|
./scan_begin_budget_test sanitize
|
|
make clean
|
|
echo "[pass] scan_begin_budget_test (TSan)"
|
|
|
|
- name: Build and run database_scan_test (ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/database
|
|
run: |
|
|
set -eu
|
|
# End-to-end oracle for tasks/task_database.c, built from the
|
|
# shipping translation units. Alongside the database pairing
|
|
# and playlist assertions it already carries, three memory
|
|
# safety invariants of the manual scan state machine are
|
|
# pinned here:
|
|
#
|
|
# - a content list whose entries exactly fill
|
|
# string_list_new()'s initial capacity must leave
|
|
# DATABASE_SCAN_ITERATE_NEXT at the M3U or the end state.
|
|
# An exhausted index must never reach
|
|
# MANUAL_SCAN_ITERATE_CONTENT, which reads its entry
|
|
# before any bounds check;
|
|
#
|
|
# - a content directory with nothing scannable in it must be
|
|
# rejected as invalid content by the database branch of
|
|
# manual_scan_begin_dir_list(), the same rule the
|
|
# non-database branch applies, instead of iterating an
|
|
# empty list;
|
|
#
|
|
# - task_database_cue_prune() and gdi_prune() must leave the
|
|
# entry being scanned alone when a cue or gdi sheet names
|
|
# itself, because the serial lookup reads that path next.
|
|
#
|
|
# The first and third are bounds and lifetime properties that
|
|
# a plain build walks straight past, so this lane is sanitized
|
|
# only and LeakSanitizer decides the teardown as well.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) database_scan_test SANITIZER=address,undefined
|
|
test -x database_scan_test
|
|
scan_root="$(mktemp -d)"
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 900 \
|
|
./database_scan_test "$scan_root"
|
|
rm -rf "$scan_root"
|
|
make clean
|
|
echo "[pass] database_scan_test (ASan)"
|
|
|
|
- name: Build and run database_scan_test (TSan, no threads, no readers)
|
|
shell: bash
|
|
working-directory: samples/tasks/database
|
|
run: |
|
|
set -eu
|
|
# The scan's reader tasks under TSan, with a stub SMB backend
|
|
# so they take smb:// paths; then the same checks
|
|
# in a build without threads, with the budget test too, and in
|
|
# one whose readers are compiled in but never start.
|
|
make clean SANITIZER=thread && make -j$(nproc) database_scan_test SANITIZER=thread \
|
|
SCAN_DEFINES=-DHAVE_SMBCLIENT
|
|
scan_root="$(mktemp -d)"
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 900 ./database_scan_test "$scan_root"
|
|
rm -rf "${scan_root:?}"
|
|
make clean && make -j$(nproc) database_scan_test scan_begin_budget_test HAVE_THREADS=0
|
|
scan_root="$(mktemp -d)"
|
|
timeout 900 ./database_scan_test "$scan_root"
|
|
rm -rf "${scan_root:?}"
|
|
budget_root="$(mktemp -d)"
|
|
TMPDIR="$budget_root" timeout 600 ./scan_begin_budget_test
|
|
rm -rf "${budget_root:?}"
|
|
make clean && make -j$(nproc) database_scan_test SANITIZER=address,undefined \
|
|
SCAN_DEFINES=-DSCAN_READERS_MAX=0
|
|
scan_root="$(mktemp -d)"
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 900 \
|
|
./database_scan_test "$scan_root"
|
|
rm -rf "${scan_root:?}"
|
|
make clean
|
|
echo "[pass] database_scan_test (TSan, HAVE_THREADS=0, SCAN_READERS_MAX=0)"
|
|
|
|
- name: Build and run pl_manager_budget_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/playlist_manager
|
|
run: |
|
|
set -eu
|
|
# Oracle for the playlist manager tasks' load phase,
|
|
# compiled from the shipping translation unit against the
|
|
# real task queue, the real playlist reader and the real
|
|
# shared per-frame I/O window, with a virtual clock so the
|
|
# pacing assertions are exact.
|
|
#
|
|
# Both handlers were already per-entry state machines with
|
|
# one exception: the load read the whole playlist in a
|
|
# single blocking call, which for a large collection is
|
|
# tens of milliseconds of JSON on the thread driving the
|
|
# frame loop whenever Threaded Tasks is off.
|
|
#
|
|
# The pacing lane isolates that phase: the per-entry
|
|
# iteration costs the same number of invocations either
|
|
# way, so the difference between a paced and an unpaced run
|
|
# is the load. The cancel lane, under LSan, covers
|
|
# abandoning a task while its playlist is still being read.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./pl_manager_budget_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./pl_manager_budget_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./pl_manager_budget_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run overlay_budget_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/overlay
|
|
run: |
|
|
set -eu
|
|
# Oracle for the overlay loader's pacing, compiled from the
|
|
# shipping translation unit against the real task queue and
|
|
# the real shared per-frame I/O window, with image loading
|
|
# stubbed and a virtual clock so the assertions are exact.
|
|
#
|
|
# The loader used to chunk by a count derived from the
|
|
# workload - half an overlay's descriptors, a quarter of
|
|
# the overlays - which is not pacing: a bigger overlay just
|
|
# meant bigger chunks, so the job always landed in the same
|
|
# two or four handler invocations however long each took.
|
|
# The scaling lane is the one that pins the fix: work per
|
|
# invocation must not grow with the size of the overlay.
|
|
#
|
|
# LSan also guards the image list, which used to be
|
|
# released only when a load was cancelled and therefore
|
|
# leaked once per successful overlay load.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./overlay_budget_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./overlay_budget_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./overlay_budget_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run overlay_visibility_bounds_test (ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/overlay
|
|
run: |
|
|
set -eu
|
|
# Builds the shipping input/input_overlay_alpha.c and
|
|
# led/drivers/led_overlay.c against a stub driver. Which images
|
|
# the overlay LED driver hides: a pack's _led images, else the
|
|
# image at a ledN_map slot whatever its desc does, and never an
|
|
# index off the page - led_map comes from the config with no
|
|
# range of its own. And the per-poll alpha pass: an image is set only when
|
|
# its alpha changes, a page load sets them all again, and an
|
|
# LED going out and back on between polls is shown again.
|
|
make -f Makefile.visibility clean >/dev/null
|
|
make -f Makefile.visibility all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./overlay_visibility_bounds_test
|
|
make -f Makefile.visibility clean >/dev/null
|
|
|
|
- name: Build and run content_prefetch_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/content_prefetch
|
|
run: |
|
|
set -eu
|
|
# Oracle for the content prefetch task - the path a menu
|
|
# content load defers to, streaming the bytes in a budgeted
|
|
# slice per tick while the frontend keeps running, so the
|
|
# load takes a finished buffer instead of blocking on a
|
|
# read.
|
|
#
|
|
# Delivery is by ownership transfer from the task callback,
|
|
# so this is as much a memory-correctness test as a
|
|
# behavioural one, and it runs under all three flavors. One
|
|
# lane drives the THREADED queue deliberately: the other
|
|
# lanes run the handler on the calling thread, where a TSan
|
|
# run would prove nothing about the worker-to-callback
|
|
# hand-off that is the only place a race could live.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./content_prefetch_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./content_prefetch_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./content_prefetch_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run bluetooth_scan_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/bluetooth_scan
|
|
run: |
|
|
set -eu
|
|
# The Bluetooth scan task on the real threaded task queue,
|
|
# with the discovery window shortened. The scan used to wait
|
|
# out the window inside one handler call, holding the task
|
|
# thread; it now begins, is parked, looks every poll whether
|
|
# the driver is done, and ends. Pins that begin and end run
|
|
# once each a window apart, that another task runs during the
|
|
# window, that the callback fires, and that a driver done
|
|
# early ends the scan then.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 60 ./bluetooth_scan_test
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 60 ./bluetooth_scan_test
|
|
echo "[pass] bluetooth_scan_test"
|
|
|
|
- name: Build and run wifi_tasks_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/wifi_tasks
|
|
run: |
|
|
set -eu
|
|
# The Wi-Fi tasks on the real threaded task queue, with a
|
|
# driver whose scan takes a while. Connect, Disconnect and the
|
|
# Wi-Fi Enabled toggle pushed during the networks list's
|
|
# periodic scan were turned away; pins that they are held and
|
|
# run after it in order, one on the driver at a time, and that
|
|
# only a scan behind a scan is refused.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
timeout 60 ./wifi_tasks_test
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 60 ./wifi_tasks_test
|
|
echo "[pass] wifi_tasks_test"
|
|
|
|
- name: Build and run wait_timeout_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/wait_timeout
|
|
run: |
|
|
set -eu
|
|
# task_queue_wait() waits indefinitely, which is wrong for
|
|
# anything depending on a machine we do not control - the
|
|
# netplay MITM query waits on an HTTP round trip to the
|
|
# lobby server, and a server that never answers used to hang
|
|
# the frontend outright.
|
|
#
|
|
# These lanes pin both directions: a wait that CAN be
|
|
# satisfied returns as soon as it is, rather than sitting
|
|
# out its timeout, and one that cannot gives up at roughly
|
|
# the deadline instead of never. Both queue implementations
|
|
# are covered, since the bound is a wrapped condition rather
|
|
# than per-implementation code.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./wait_timeout_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./wait_timeout_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./wait_timeout_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run task_ordering_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/ordering
|
|
run: |
|
|
set -eu
|
|
# Pins the task queue's visibility contract, which is what
|
|
# made a movie-record ordering guard fail.
|
|
#
|
|
# retro_task_regular_gather() lifts every running task off
|
|
# the queue and puts each one BACK as it processes it, so
|
|
# whether a finder called from inside a handler sees a
|
|
# sibling depends on where that sibling sits in the pass.
|
|
# At least one invocation reports "nothing in progress"
|
|
# while the task is genuinely pending - which an ordering
|
|
# guard only has to get wrong once. A main-thread flag is
|
|
# visible from inside a handler and is what the guards use
|
|
# now; the ordering lane fails if a task guarded that way
|
|
# runs before the one it waits on.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./task_ordering_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./task_ordering_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./task_ordering_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run autodetect_handoff_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/autodetect_handoff
|
|
run: |
|
|
set -eu
|
|
# Autoconfig reports from a driver's own thread are made on
|
|
# the main thread, in order, where the queue's answer is exact.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./autodetect_handoff_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./autodetect_handoff_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./autodetect_handoff_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run task_watchdog_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/watchdog
|
|
run: |
|
|
set -eu
|
|
# Oracle for the task queue's slow-handler watchdog,
|
|
# compiled against the shipping queue. With Threaded Tasks
|
|
# off, handlers run on the thread that drives the frame
|
|
# loop, so a handler that does not return promptly is a
|
|
# visible stall - and the queue is the only place that can
|
|
# attribute one to a specific task instead of to "something
|
|
# in the frame".
|
|
#
|
|
# The lanes pin: a handler inside its budget is silent; one
|
|
# that overruns is reported once, attributed to that task,
|
|
# with a duration above the budget; the threaded queue is
|
|
# deliberately not measured, because a long handler on a
|
|
# worker is what the worker is for; and an unregistered
|
|
# watchdog never fires, which is the default and the state
|
|
# release builds ship in.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./task_watchdog_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./task_watchdog_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=thread -j$(nproc)
|
|
TSAN_OPTIONS=halt_on_error=1 ./task_watchdog_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run nat_task_serialisation_test (plain, ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/netplay_nat
|
|
run: |
|
|
set -eu
|
|
# Oracle for the NAT traversal task's request serialisation,
|
|
# compiled from the shipping translation unit against the
|
|
# real task queue with the natt_* device layer faked.
|
|
#
|
|
# Only one NAT task may run at a time - both requests mutate
|
|
# the single shared request object - and that exclusion used
|
|
# to be enforced by blocking the calling thread, which is
|
|
# the one driving the menu, for the duration of UPnP
|
|
# discovery. The lanes pin the replacement: a push issued
|
|
# while a task is in flight returns without running it to
|
|
# completion, a close followed by an open runs as both in
|
|
# that order rather than being coalesced (coalescing would
|
|
# leak the port mapping), queued requests actually start
|
|
# from the task callbacks, and an overflowing request is
|
|
# refused rather than dropped.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./nat_task_serialisation_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./nat_task_serialisation_test
|
|
make clean >/dev/null
|
|
|
|
- name: Guard against unbounded task_queue_wait in network/ and tasks/
|
|
shell: bash
|
|
run: |
|
|
set -eu
|
|
# task_queue_wait(NULL, ...) does not mean "wait for my
|
|
# task" - a NULL condition makes both queue implementations
|
|
# loop until the queue is EMPTY. A caller blocking on one
|
|
# HTTP round trip therefore also waits out every unrelated
|
|
# task in flight: a content scan, a core download, a
|
|
# thumbnail pack. Where a wait is still necessary it must
|
|
# name a condition that describes the caller's own work, so
|
|
# the block is bounded by that work and nothing else.
|
|
#
|
|
# This does not sanction waiting - the menu paths are held
|
|
# to the stricter no-wait guard below. It stops the
|
|
# unbounded form from coming back where waits remain.
|
|
hits=$(grep -rn --include='*.c' --include='*.cpp' \
|
|
'task_queue_wait[[:space:]]*([[:space:]]*NULL' \
|
|
network/ tasks/ || true)
|
|
if [[ -n "$hits" ]]; then
|
|
printf '%s\n' "$hits"
|
|
printf '::error title=Unbounded task_queue_wait::Pass a condition describing your own work; NULL waits for the entire queue to drain.\n'
|
|
exit 1
|
|
fi
|
|
printf 'No unbounded task_queue_wait in network/ or tasks/.\n'
|
|
|
|
- name: Guard against task_queue_wait on interactive menu paths
|
|
shell: bash
|
|
run: |
|
|
set -eu
|
|
# task_queue_wait() spins the calling thread until a task
|
|
# finishes. Reached from the menu it is a hard UI freeze
|
|
# for the duration of the work - a database scan on slow
|
|
# storage is seconds - and on Android it is an ANR. The
|
|
# menu teardown waits that used to live here were replaced
|
|
# by cancellation plus a stale-completion generation guard
|
|
# (menu_explore_cancel_init_task / menu_dbinfo_cancel_task).
|
|
#
|
|
# Scope is mechanical: menu/, the task translation units the
|
|
# menu drives directly, and the content load - a chain of
|
|
# main-thread tasks that closes content from the frame loop.
|
|
# The remaining wait sites (NAT traversal, the CLI database
|
|
# scan, the exit path) are tracked separately; widening this
|
|
# guard is what should happen as each of those is converted.
|
|
hits=$(grep -rn 'task_queue_wait' menu/ network/ command.c \
|
|
tasks/task_menu_explore.c \
|
|
tasks/task_database_info.c \
|
|
tasks/task_netplay_nat_traversal.c \
|
|
tasks/task_content.c || true)
|
|
if [[ -n "$hits" ]]; then
|
|
printf '%s\n' "$hits"
|
|
printf '::error title=Blocking wait on a menu path::task_queue_wait() must not be reachable from the menu. Use cancellation with a stale-completion guard, or a deferred continuation.\n'
|
|
exit 1
|
|
fi
|
|
# The close's blocking helpers wait inside task_save.c, where
|
|
# the grep above cannot see them: the menu and the tasks must
|
|
# not call them. The load closes through
|
|
# retroarch_main_deinit_begin/pending/finish instead.
|
|
hits=$(grep -rn 'content_wait_for_\(save\|load\)_state_task' \
|
|
menu/ tasks/ --include='*.c' \
|
|
| grep -v '^tasks/task_save.c:' || true)
|
|
if [[ -n "$hits" ]]; then
|
|
printf '%s\n' "$hits"
|
|
printf '::error title=Blocking close on a menu path::content_wait_for_*_state_task() blocks the frame loop. Close through retroarch_main_deinit_begin/pending/finish from a main-thread task.\n'
|
|
exit 1
|
|
fi
|
|
grep -q 'retroarch_main_deinit_pending' tasks/task_content.c || {
|
|
printf '::error title=Blocking close on a menu path::the content load no longer waits for state tasks from the frame loop.\n'
|
|
exit 1
|
|
}
|
|
printf 'No task_queue_wait or blocking close on the guarded menu paths.\n'
|
|
|
|
- name: Build and run playlist_parity_test (plain, ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/playlist
|
|
run: |
|
|
set -eu
|
|
# Parity oracle for playlist.c's read path (playlist_init /
|
|
# playlist_read_file), compiled from the shipping
|
|
# translation unit. Pins the reader's observable behaviour
|
|
# ahead of its restructuring into a budgeted resumable
|
|
# parse: the full JSON schema, the
|
|
# malformed-JSON-keeps-partial-data contract, the old
|
|
# six-line format with its trailing metadata block, RZIP
|
|
# compressed input, missing-file-reads-empty, and capacity
|
|
# clamping. The budgeted lanes then assert the resumable
|
|
# API (playlist_parse_begin/step/end/abort) produces
|
|
# playlists identical to the blocking path while yielding
|
|
# repeatedly, for JSON, the old format and the autofix
|
|
# pass, and that abandoning a parse mid-document leaks
|
|
# nothing. The cache-reuse lane guards the size stamp the
|
|
# cache keys on: it must be the on-disk size for every
|
|
# format and compression combination - including after the
|
|
# autofix pass saves the file it fixed - or the cached
|
|
# playlist is never reused and the menu re-parses on every
|
|
# navigation.
|
|
#
|
|
# The switch and supersede lanes cover a field report -
|
|
# MaterialUI on Android showing one playlist's titles under
|
|
# another's heading, where selecting a row launches content
|
|
# from the wrong system. The deferred cached init yields
|
|
# mid-parse, and the menu reads playlist_get_cached() while
|
|
# building its list, so leaving the previous playlist
|
|
# installed until the new one is ready is user-visible and
|
|
# wrong. The lanes assert on EVERY yield that the cache is
|
|
# either empty or already the requested playlist, including
|
|
# when a request is superseded before it finishes, and that
|
|
# the parse really yielded - a lane that stops exercising
|
|
# its own window is worse than no lane.
|
|
#
|
|
# The pump and rebuild-reuse lanes cover issue #19427
|
|
# (large playlists flashing the empty-list placeholder):
|
|
# a yielded read must complete on frames alone, and the
|
|
# rebuild that follows completion must be served from the
|
|
# cache just installed - a restart there loops, one flash
|
|
# per completion. Pinned: the recorded
|
|
# base_content_directory under an autofix-off config, and
|
|
# the size stamp left stale by the autofix pass's save.
|
|
#
|
|
# The truncated-JSON lane also guards the staged-entry
|
|
# cleanup in playlist_read_file: an entry interrupted
|
|
# mid-object is reserved but never committed, and its
|
|
# already-parsed members must be freed on the error path -
|
|
# the leak the lane originally flushed out. LSan gates it
|
|
# here permanently. No threads on this path, so no TSan
|
|
# flavor.
|
|
make clean >/dev/null
|
|
make all -j$(nproc)
|
|
./playlist_parity_test
|
|
make clean >/dev/null
|
|
make all SANITIZER=address,undefined -j$(nproc)
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
|
|
./playlist_parity_test
|
|
make clean >/dev/null
|
|
|
|
- name: Build and run dirwalk_budget_test (plain, ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/menu/dirwalk
|
|
run: |
|
|
set -eu
|
|
# Oracle for menu/menu_dirwalk.c, the budgeted, cancellable
|
|
# directory enumeration behind interactive menu list
|
|
# building, compiled from the tree so it exercises the
|
|
# shipping translation unit.
|
|
#
|
|
# The regression it pins: filebrowser_parse and the
|
|
# playlist-directory displaylists walk and sort their
|
|
# directory synchronously on the thread that drives the UI,
|
|
# a stall proportional to directory size. menu_dirwalk
|
|
# caps it: one budgeted synchronous pass under the shared
|
|
# per-frame I/O window (small and medium directories
|
|
# complete on the spot, identical result, single tick),
|
|
# with oversized walks - and oversized sorts, which for a
|
|
# 40k-entry directory cost more than the walk - continuing
|
|
# on a task as a resumable enumeration plus a resumable
|
|
# bottom-up mergesort, ordered by the exact comparator
|
|
# dir_list_sort() uses.
|
|
#
|
|
# The clock is the test's instrument (the save_state
|
|
# pattern): the sample provides cpu_features_get_time_usec()
|
|
# itself. Step 0 reads the real clock for the pacing and
|
|
# bench lanes; a fixed virtual step per observation makes
|
|
# window exhaustion deterministic for the deferral, cancel
|
|
# and threaded lanes on any machine at any cache
|
|
# temperature. The build also shrinks the sort-machinery
|
|
# constants (run length, merge batch, synchronous-sort
|
|
# ceiling) so run sorting, the element-granular resumable
|
|
# merge, and sort-only deferral are all driven across many
|
|
# invocations by a 200-entry fixture.
|
|
#
|
|
# Parity is the contract: a consumed listing equals
|
|
# dir_list_new() plus the same sort, entry for entry with
|
|
# attributes, across the flag and sort-mode combinations
|
|
# the displaylist callers use. The pacing lane asserts no
|
|
# single main-thread stall - request, task gather, or
|
|
# consume - exceeds the window plus bounded slack on a
|
|
# 40k-file flat fixture (the walk under test is
|
|
# non-recursive: the menu browses one level, so the files
|
|
# sit flat, which is also what a large ROM directory looks
|
|
# like). The bench lane holds the hard precondition:
|
|
# wall-clock from first request to consumed result may not
|
|
# regress past the blocking walk + sort it replaces. The
|
|
# cancel lane supersedes and cancels mid-walk and drains
|
|
# stale completions, with LeakSanitizer judging the
|
|
# teardown; the threaded lane hands the walk to a worker
|
|
# and is the TSan target for the handler-to-main-callback
|
|
# hand-off.
|
|
make clean && make -j$(nproc) all
|
|
test -x dirwalk_budget_test
|
|
timeout 600 ./dirwalk_budget_test
|
|
echo "[pass] dirwalk_budget_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 900 \
|
|
./dirwalk_budget_test sanitize
|
|
echo "[pass] dirwalk_budget_test (ASan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 900 \
|
|
./dirwalk_budget_test sanitize
|
|
make clean
|
|
echo "[pass] dirwalk_budget_test (TSan)"
|
|
|
|
- name: Build and run save_state_io_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/save_state
|
|
run: |
|
|
set -eu
|
|
# Oracle for the file I/O in tasks/task_save.c, compiled
|
|
# from the tree so it exercises the shipping translation
|
|
# unit. As in image_decode_slice, the clock is the test's
|
|
# instrument: the sample provides
|
|
# cpu_features_get_time_usec() itself, advancing a fixed
|
|
# step per observation, so the tick budget is assertable
|
|
# exactly rather than measured against a wall clock. Step 0
|
|
# is a budget that never expires; step >= the budget is one
|
|
# that expires on first observation.
|
|
#
|
|
# The throughput regression it pins: both handlers used to
|
|
# transfer exactly one SAVE_STATE_CHUNK per queue tick,
|
|
# which caps a save at chunk * tick_rate (~6 MB/s) whatever
|
|
# the device can do - a 16 MB state took 164 ticks, 2.7s at
|
|
# 60Hz, while one 100 KB write costs ~62us of a 16667us
|
|
# frame. A tick is now bounded by time and transfers as
|
|
# many quanta as fit. Because a time budget is only an
|
|
# improvement if it cannot be worse than the fixed count it
|
|
# replaced, the exhausted-budget lane asserts the fallback
|
|
# directly: with a clock that spends the budget on its first
|
|
# observation the handler must still transfer exactly one
|
|
# quantum per tick and still produce a correct file. That
|
|
# is the old behaviour reached through the new code, and it
|
|
# is what the slowest supported storage gets.
|
|
#
|
|
# The rest are the defects the audit turned up, each
|
|
# verified to fail this oracle on the pre-fix tree:
|
|
#
|
|
# - The rastate block walker looped on 'input < stop', so
|
|
# it could enter the body with one byte left and read the
|
|
# block length out of input[4..7] - six bytes past the
|
|
# end. content_deserialize_state had the same shape one
|
|
# level up, reading seven bytes of magic and input[7] for
|
|
# the version with no length check at all. Savestates
|
|
# are shared, synced and recovered off failing media, and
|
|
# the load handler's own short-read path produces exactly
|
|
# this buffer, so this is a format parser on hostile
|
|
# input. ASan reports both for any state truncated to
|
|
# 9..15 bytes.
|
|
#
|
|
# - A block's declared 32-bit length was never checked
|
|
# against the bytes actually remaining and was then
|
|
# passed to core_unserialize as the size of the buffer at
|
|
# that offset. The stub core here reads every byte it is
|
|
# told the buffer holds before judging the size, exactly
|
|
# as a real core does, so ASan - not the stub's own
|
|
# opinion - is what catches an over-long block. A stub
|
|
# that checked size first would pass against the unfixed
|
|
# walker.
|
|
#
|
|
# - That length was also assembled as input[7] << 24 on an
|
|
# int, undefined for every top byte >= 0x80, i.e. every
|
|
# length at or above 2 GB - the range a corrupt length
|
|
# lands in, and the value the bound check is asked to
|
|
# trust. UBSan is the assertion.
|
|
#
|
|
# - A failed core_serialize left data NULL and size 0, at
|
|
# which point every test in the save handler read as
|
|
# success and the task reported COMPLETED. The file had
|
|
# already been opened and truncated, so the user was told
|
|
# the save succeeded and the slot held zero bytes.
|
|
#
|
|
# - A failed open was a bare return: not errored, not
|
|
# finished, so the queue re-entered the task every tick
|
|
# and it retried forever. Save tasks are
|
|
# TASK_TYPE_BLOCKING, so that one task held the single
|
|
# blocking slot for the rest of the session - no state
|
|
# could be saved or loaded again without a restart. The
|
|
# exclusion rule that makes it severe is itself pinned,
|
|
# together with the cleanup the refusal path owes (the
|
|
# task, its title, the state struct and the caller's
|
|
# serialized buffer), which is why leak detection is on.
|
|
#
|
|
# The undo-snapshot lane is the one that needs the counting
|
|
# allocator (-Wl,--wrap=malloc in the sample's Makefile).
|
|
# content_load_state_cb retakes the undo snapshot on every
|
|
# state load, and that capture used to free its destination
|
|
# and malloc a new one each time; it now keeps the outgoing
|
|
# allocation as a spare and swaps the two. Pointer identity
|
|
# cannot see the difference - a state-sized block is mmap'd,
|
|
# munmap hands the region back, and the next request of the
|
|
# same size is usually given the same address - so a run of
|
|
# captures looks identical either way while only one of them
|
|
# pays the first-touch faults. Counting state-sized malloc
|
|
# calls is the discriminator: eight captures allocate two
|
|
# buffers after the change and eight before it.
|
|
#
|
|
# The conc lane runs the same round-trip under a threaded
|
|
# queue, where the transfer loop executes on the worker
|
|
# while content_deserialize_state and the undo bookkeeping
|
|
# stay on the gathering thread. It is one task at a time by
|
|
# design, not by simplification: TASK_TYPE_BLOCKING means
|
|
# the frontend cannot produce concurrent save tasks, so a
|
|
# lane that pushed them would be asserting against a
|
|
# configuration that cannot occur. What it asks TSan is
|
|
# whether a loop that newly carries a deadline across
|
|
# iterations kept any of it shared.
|
|
make clean && make -j$(nproc) all
|
|
test -x save_state_io_test
|
|
timeout 300 ./save_state_io_test
|
|
timeout 300 ./save_state_io_test conc
|
|
echo "[pass] save_state_io_test"
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 \
|
|
./save_state_io_test
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
|
|
timeout 300 ./save_state_io_test conc
|
|
echo "[pass] save_state_io_test (ASan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 \
|
|
./save_state_io_test
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 \
|
|
./save_state_io_test conc
|
|
echo "[pass] save_state_io_test (TSan)"
|
|
# The same oracle with HAVE_COMPRESSION, as the frontend
|
|
# ships it, states going through rzip. The lane that needs
|
|
# it runs only here: a save over a slot keeps the file that
|
|
# was there for undo, and an undo puts that file back byte
|
|
# for byte - compressed or not, whatever the setting has
|
|
# become - rather than inflating it on every save and
|
|
# deflating it afresh on undo.
|
|
make clean RZIP=1 && make -j$(nproc) all RZIP=1 SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 \
|
|
./save_state_io_test_rzip
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
|
|
timeout 300 ./save_state_io_test_rzip conc
|
|
echo "[pass] save_state_io_test_rzip (ASan)"
|
|
|
|
- name: ccache statistics
|
|
if: ${{ !cancelled() }}
|
|
continue-on-error: true
|
|
run: ccache -sv
|
|
|
|
- name: Save ccache
|
|
# Only master's pushes keep a cache: pull requests restore
|
|
# master's, so one push does not evict the next one's.
|
|
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: ~/.ccache
|
|
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
|
|
|
|
samples-tasks-http:
|
|
name: Build and run samples/tasks/http
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
APT_PKGS: build-essential zlib1g-dev ccache
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Apt cache key
|
|
id: apt
|
|
run: sh .github/workflows/scripts/apt-cache-key.sh "$APT_PKGS"
|
|
|
|
- name: Restore apt cache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: ~/.apt-cache
|
|
key: ${{ steps.apt.outputs.key }}
|
|
restore-keys: ${{ steps.apt.outputs.prefix }}
|
|
|
|
- name: Install dependencies
|
|
timeout-minutes: 20
|
|
run: sh .github/workflows/scripts/apt-install.sh $APT_PKGS
|
|
|
|
- name: Save apt cache
|
|
if: env.APT_CACHE_SAVE == '1'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: ~/.apt-cache
|
|
key: ${{ steps.apt.outputs.key }}
|
|
|
|
- name: Link with lld
|
|
run: sh .github/workflows/scripts/use-lld.sh
|
|
|
|
- name: Restore ccache
|
|
uses: actions/cache/restore@v4
|
|
with:
|
|
path: ~/.ccache
|
|
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
|
|
restore-keys: |
|
|
ccache-${{ github.workflow }}-${{ github.job }}-
|
|
|
|
- name: Enable ccache
|
|
run: |
|
|
set -eu
|
|
export CCACHE_DIR="$HOME/.ccache"
|
|
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
|
|
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
|
|
ccache -M 250M
|
|
ccache -z
|
|
|
|
- name: C89 syntax check for the HTTP task translation units
|
|
shell: bash
|
|
run: |
|
|
set -eu
|
|
# Fast guard for the C89 dialect rules these files are built
|
|
# under. RetroArch's full C89 build lives in retroarch.yml
|
|
# (make C89_BUILD=1), which does catch violations but only
|
|
# after a configure and a complete build; a syntax-only pass
|
|
# over the handful of translation units this area touches
|
|
# fails in seconds instead.
|
|
#
|
|
# It exists because a mixed declaration slipped into
|
|
# task_core_updater.c and reached master: the C99-style
|
|
# "declare where first used" habit is silent under the
|
|
# default build and only surfaces here.
|
|
# -Werror=declaration-after-statement is the specific rule
|
|
# that catches it; the rest of the flag set mirrors the
|
|
# C89_BUILD block in the top-level Makefile. _GNU_SOURCE is
|
|
# added because -ansi otherwise hides the POSIX prototypes
|
|
# (strdup and friends) that these files legitimately use,
|
|
# which would produce a wall of int-conversion noise
|
|
# unrelated to the dialect rules being checked.
|
|
C89="-std=c89 -ansi -pedantic -Werror=pedantic -Wno-long-long"
|
|
C89="$C89 -Werror=declaration-after-statement -Wno-variadic-macros"
|
|
C89="$C89 -D_GNU_SOURCE"
|
|
INC="-Ilibretro-common/include -I. -Ideps"
|
|
DEF="-DRARCH_INTERNAL -DHAVE_NETWORKING -DHAVE_THREADS -DHAVE_COMPRESSION"
|
|
rc=0
|
|
for f in libretro-common/net/net_http.c \
|
|
libretro-common/queues/task_queue.c \
|
|
tasks/task_http.c \
|
|
tasks/task_core_updater.c \
|
|
tasks/task_pl_thumbnail_download.c \
|
|
tasks/task_core_backup.c \
|
|
core_backup.c \
|
|
libretro-common/streams/interface_stream.c \
|
|
tasks/task_file_transfer.c \
|
|
tasks/task_nbio_slice.c \
|
|
libretro-common/lists/dir_list.c \
|
|
libretro-common/formats/logiqx_dat/logiqx_dat.c \
|
|
libretro-common/formats/xml/rxml.c \
|
|
database_info.c; do
|
|
if gcc -fsyntax-only $C89 $INC $DEF "$f"; then
|
|
echo "[pass] C89 $f"
|
|
else
|
|
echo "::error title=C89 violation::$f"
|
|
rc=1
|
|
fi
|
|
done
|
|
exit $rc
|
|
|
|
- name: Build and run http_url_dedup_test (ASan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# Regression test for the duplicate-download suppression key
|
|
# in tasks/task_http.c and tasks/task_http_emscripten.c.
|
|
# connection_url was a fixed char[NAME_MAX_LENGTH] filled by
|
|
# strlcpy, but task_http_finder() compares that stored copy
|
|
# against the raw candidate URL - so past NAME_MAX_LENGTH the
|
|
# two could never compare equal and the "concurrent download
|
|
# of the same file is not allowed" guard returned false for
|
|
# every candidate. Playlist thumbnail URLs exceed it
|
|
# routinely, and both tasks then reached their completion
|
|
# callback and raced filestream_write_file() over one path.
|
|
#
|
|
# The failure is one-directional: truncation cannot conflate
|
|
# two distinct URLs, since the candidate is never truncated
|
|
# to match. So the test asserts both that the owned key
|
|
# self-matches at any length and that distinct long URLs
|
|
# stay distinct - the latter guarding against a future "fix"
|
|
# that hashes the key to a fixed width and introduces the
|
|
# false positive this bug did not have. It keeps the old
|
|
# fixed-buffer shape as a second oracle so a revert is
|
|
# caught. Both NAME_MAX_LENGTH values are covered: 256 on
|
|
# most targets, 128 on the small-path platforms.
|
|
make -f Makefile.dedup clean SANITIZER=address && make -f Makefile.dedup -j$(nproc) all SANITIZER=address
|
|
timeout 60 ./http_url_dedup_test
|
|
make -f Makefile.dedup clean SANITIZER=address && make -f Makefile.dedup -j$(nproc) all SANITIZER=address \
|
|
CFLAGS_EXTRA=-DNAME_MAX_LENGTH=128
|
|
timeout 60 ./http_url_dedup_test
|
|
make -f Makefile.dedup clean
|
|
echo "[pass] http_url_dedup_test"
|
|
|
|
- name: Build and run http_emscripten_park_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http_emscripten
|
|
run: |
|
|
set -eu
|
|
# tasks/task_http_emscripten.c built natively against stand-in
|
|
# Emscripten headers: the task parks while its fetch is in
|
|
# flight and the fetch's settle wakes it.
|
|
make clean SANITIZER=address,undefined && make -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
|
|
timeout 60 ./http_emscripten_park_test
|
|
echo "[pass] http_emscripten_park_test (ASan + UBSan)"
|
|
make clean SANITIZER=thread && make -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 60 ./http_emscripten_park_test
|
|
echo "[pass] http_emscripten_park_test (TSan)"
|
|
|
|
- name: Build and run http_emscripten_headers_test (ASan + UBSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# Header marshalling for tasks/task_http_emscripten.c, which
|
|
# sits between two representations: RetroArch's raw
|
|
# CRLF-delimited blob and the NULL-terminated key/value array
|
|
# emscripten_fetch wants, plus the response blob that has to
|
|
# become the same "Name: Value" header block net_http.c
|
|
# produces - through net_http_headers_compact(), which the test
|
|
# links rather than copies. Neither conversion existed while the backend was
|
|
# on emscripten_async_wget2_data, which has no header support
|
|
# at all - the `headers` argument was accepted and dropped on
|
|
# every entry point, so cloud sync could not authenticate.
|
|
#
|
|
# The parsing is where the sharp edges are: hand-built blobs
|
|
# in task_push_webdav_move() and
|
|
# task_push_http_transfer_with_content() mean bare LF,
|
|
# missing trailing CRLF, empty values, embedded colons and
|
|
# malformed lines all reach it. A header dropped here is an
|
|
# auth failure rather than a crash - silent, and awkward to
|
|
# trace back - so the degenerate cases are worth pinning.
|
|
# Pure logic, so it runs natively without emcc.
|
|
make -f Makefile.emsc_headers clean SANITIZER=address && make -f Makefile.emsc_headers -j$(nproc) all SANITIZER=address
|
|
timeout 60 ./http_emscripten_headers_test
|
|
make -f Makefile.emsc_headers clean SANITIZER=undefined && make -f Makefile.emsc_headers -j$(nproc) all SANITIZER=undefined
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
|
|
./http_emscripten_headers_test
|
|
make -f Makefile.emsc_headers clean
|
|
echo "[pass] http_emscripten_headers_test"
|
|
|
|
- name: Build and run http_task_lifetime_test (ASan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# Lifetime contract for task->state between tasks/task_http.c
|
|
# and libretro-common/queues/task_queue.c. Links the real
|
|
# task_http.c and the real task_queue.c; only
|
|
# msg_hash_to_str() and task_window_progress_cb() are
|
|
# stubbed, since neither participates in lifetime.
|
|
#
|
|
# This is the lane the other HTTP samples could not cover:
|
|
# they drive net_http.c directly, which exercises the
|
|
# protocol engine well and the task layer not at all - and
|
|
# the task layer is where the crash was.
|
|
# task_http_transfer_handler() freed its http_handle_t as
|
|
# soon as it set RETRO_TASK_FLG_FINISHED, but a task stays
|
|
# findable long after its handler is done:
|
|
# retro_task_threaded_find() scans the running, finished AND
|
|
# retiring lists, and task_http_finder() dereferences
|
|
# task->state on every candidate. So task->state dangled for
|
|
# the whole window between the last handler tick and full
|
|
# retirement, and any concurrent push racing a finishing
|
|
# download read freed memory.
|
|
#
|
|
# It survived for years because connection_url was an inline
|
|
# char[NAME_MAX_LENGTH]: reading a freed-but-still-mapped
|
|
# array yields stale bytes and strcmp() plods through them.
|
|
# Making connection_url a pointer turned the same read into
|
|
# "load the freed chunk's tombstone and dereference it" - a
|
|
# hard SIGSEGV inside task_http_finder on Windows. The
|
|
# pointer change did not introduce the defect; it removed the
|
|
# padding hiding it.
|
|
#
|
|
# The shape is the one from that backtrace: threads push GETs
|
|
# for a small set of URLs while the queue finishes and
|
|
# retires earlier ones, so every push runs task_queue_find()
|
|
# over all three lists and collides with retirement
|
|
# continuously. ASan is what makes it assertable - without
|
|
# it a use-after-free on a recently freed chunk reads
|
|
# plausible bytes and passes. TSan matters as much, since
|
|
# the race is what makes the window reachable.
|
|
#
|
|
# To confirm this test still bites, move the teardown in
|
|
# task_http.c from task_http_transfer_cleanup() back into
|
|
# task_http_transfer_handler() and rerun under ASan.
|
|
make -f Makefile.task_lifetime clean SANITIZER=address && make -f Makefile.task_lifetime -j$(nproc) all SANITIZER=address
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
timeout 300 ./http_task_lifetime_test
|
|
echo "[pass] http_task_lifetime_test (ASan)"
|
|
make -f Makefile.task_lifetime clean SANITIZER=thread && make -f Makefile.task_lifetime -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 \
|
|
./http_task_lifetime_test
|
|
make -f Makefile.task_lifetime clean
|
|
echo "[pass] http_task_lifetime_test (TSan)"
|
|
|
|
- name: Build and run http_transfer_stress_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# Transport-level oracle for libretro-common/net/net_http.c
|
|
# as driven by tasks/task_http.c, against a real loopback
|
|
# server. Framing correctness comes first - Content-Length,
|
|
# chunked across chunk sizes 1..65536, and
|
|
# connection-close-delimited, then the same again with the
|
|
# server dribbling a few bytes per write so every header
|
|
# line, chunk header and chunk boundary is torn across
|
|
# receive calls.
|
|
#
|
|
# Two things make the tick-cost lane mean anything. It
|
|
# interposes socket() to pin SO_RCVBUF, because per-update
|
|
# byte yield otherwise tracks host /proc/sys/net tuning and
|
|
# autoscaling; and it sleeps the task-queue tick period
|
|
# between updates, because without that the client polls
|
|
# faster than the peer can fill the socket, every recv()
|
|
# takes a sliver, and the number reports the server's write
|
|
# rate rather than the structural per-tick cap - it swung
|
|
# between 263 and 93207 B/tick across runs. Sleeping the
|
|
# period lets the receive buffer saturate, which is the
|
|
# regime RetroArch actually runs in and the only one where
|
|
# ticks-per-body is stable.
|
|
#
|
|
# The peak-window ceiling for the streaming sink is paired
|
|
# with a contrast case asserting the buffered path *exceeds*
|
|
# it, so the pair cannot pass by both paths happening to stay
|
|
# small on a given host. The teardown lane deinits,
|
|
# re-inits and transfers again: calling net_http_deinit() is
|
|
# what makes a leak in the DNS cache and connection pool
|
|
# visible to LeakSan at all, since both lists hang off
|
|
# file-scope globals and otherwise count as still reachable.
|
|
make -f Makefile.stress clean SANITIZER=address,undefined && make -f Makefile.stress -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 600 \
|
|
./http_transfer_stress_test
|
|
echo "[pass] http_transfer_stress_test (ASan)"
|
|
make -f Makefile.stress clean SANITIZER=thread && make -f Makefile.stress -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 600 \
|
|
./http_transfer_stress_test
|
|
make -f Makefile.stress clean
|
|
echo "[pass] http_transfer_stress_test (TSan)"
|
|
|
|
- name: Build and run http_transport_failure_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# What a failed transfer is allowed to hand back, across
|
|
# libretro-common/net/net_http.c and tasks/task_http.c.
|
|
#
|
|
# net_http_new() allocates response.data up front as the
|
|
# receive buffer - 64KiB of plain malloc(). On a transport
|
|
# failure nothing is written into it and response.len stays
|
|
# 0, but the pointer is non-NULL, and net_http_data()'s
|
|
# accept_err returned it, so every failed download published
|
|
# uninitialised, unterminated heap as
|
|
# http_transfer_data_t::data. On a torn body it was worse:
|
|
# the caller got the Content-Length remainder as the length,
|
|
# over bytes that never arrived. Meanwhile task_http.c gated
|
|
# task_set_error() on RETRO_TASK_FLG_MUTE, which only ever
|
|
# suppressed the on-screen notification - so muted transfers
|
|
# (achievements, netplay lobby, core list refresh) reported
|
|
# failure to their callbacks as success.
|
|
#
|
|
# Four failure lanes entering net_http_update() at four
|
|
# different points, plus two contrast lanes: a 200 must still
|
|
# deliver body, headers and status, and a 404 must still
|
|
# deliver its body under accept_err, or the fix could be
|
|
# satisfied by an accessor that returns NULL rather more
|
|
# often than that and cloud sync's WWW-Authenticate path
|
|
# would break silently. Both contrast lanes pass on the
|
|
# pre-fix tree, which is what makes them a guard. Two task
|
|
# lanes run the same failure through the real task_http.c and
|
|
# task_queue.c, muted and unmuted.
|
|
#
|
|
# getaddrinfo is wrapped at link time so the DNS lane cannot
|
|
# quietly start testing a successful lookup on a runner whose
|
|
# resolver answers for everything. Leak detection is on
|
|
# because the fix changes ownership: net_http_delete() is now
|
|
# the only thing that frees the receive buffer on failure.
|
|
#
|
|
# To confirm this still bites, drop the `status < 0` guard
|
|
# from net_http_data() and rerun.
|
|
make -f Makefile.transport_fail clean SANITIZER=address,undefined && make -f Makefile.transport_fail -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 \
|
|
./http_transport_failure_test
|
|
echo "[pass] http_transport_failure_test (ASan)"
|
|
make -f Makefile.transport_fail clean SANITIZER=thread && make -f Makefile.transport_fail -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 \
|
|
./http_transport_failure_test
|
|
make -f Makefile.transport_fail clean
|
|
echo "[pass] http_transport_failure_test (TSan)"
|
|
|
|
- name: Build and run http_resume_test (ASan + UBSan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# An interrupted download to a file (task_push_http_download_file,
|
|
# as the core updater and thumbnails use) resumes with
|
|
# "Range: bytes=N-" and If-Range on the first response's strong
|
|
# ETag. A 206 from the right offset continues the file; a 200
|
|
# (resource changed) starts it over; a 206 from the wrong offset,
|
|
# a first response without Accept-Ranges or a validator, and a
|
|
# connection that keeps dropping (more than three resumes) fail
|
|
# and remove the partial file. Every interrupted download used
|
|
# to fail outright.
|
|
make -f Makefile.resume clean SANITIZER=address,undefined && make -f Makefile.resume -j$(nproc) all SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
|
|
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 300 \
|
|
./http_resume_test
|
|
echo "[pass] http_resume_test (ASan)"
|
|
make -f Makefile.resume clean SANITIZER=thread && make -f Makefile.resume -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./http_resume_test
|
|
make -f Makefile.resume clean
|
|
echo "[pass] http_resume_test (TSan)"
|
|
|
|
- name: Build and run HTTP initialization failure test
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
for threads in 0 1; do
|
|
make -f Makefile.init_failure clean
|
|
make -f Makefile.init_failure THREADS=$threads
|
|
timeout 60 ./http_init_failure_test
|
|
done
|
|
make -f Makefile.init_failure clean
|
|
|
|
- name: Build and run http_limits_test (ASan + UBSan, threaded and not)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# net_http.c included whole, with the clock and the socket
|
|
# calls stubbed. The DNS cache is capped (it grew by one entry
|
|
# per distinct host:port for the whole session), evicts least
|
|
# recently used first, and never evicts a lookup still in
|
|
# flight. The pool keeps at most 8 idle connections, closing
|
|
# the longest-idle first, and closes rather than reuses one
|
|
# idle for more than 30 s. On a system without IPv6, an IPv6
|
|
# address listed first no longer fails the transfer, and one
|
|
# mid-list no longer hides the addresses after it. Bracketed
|
|
# IPv6 literals ([::1]:8080) parse in URLs and redirects and
|
|
# reach the resolver without their brackets.
|
|
for threads in 1 0; do
|
|
make -f Makefile.limits clean
|
|
make -f Makefile.limits THREADS=$threads SANITIZER=address,undefined
|
|
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
|
|
timeout 60 ./http_limits_test
|
|
done
|
|
make -f Makefile.limits clean
|
|
|
|
- name: Build and run HTTP DNS connect lifetime test
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
for ssl in 0 1; do
|
|
make -f Makefile.dns_lifetime clean
|
|
make -f Makefile.dns_lifetime SSL=$ssl
|
|
timeout 60 ./http_dns_lifetime_test
|
|
done
|
|
make -f Makefile.dns_lifetime clean
|
|
|
|
- name: Build and run http_dns_evict_deadlock_test (ASan, TSan)
|
|
shell: bash
|
|
working-directory: samples/tasks/http
|
|
run: |
|
|
set -eu
|
|
# Deadlock regression for
|
|
# net_http_dns_cache_remove_expired(), which runs with the
|
|
# DNS cache lock held and used to sthread_join() the resolver
|
|
# thread of any entry it evicted. net_http_resolve() takes
|
|
# that same lock twice, so a join against a thread blocked
|
|
# acquiring the lock the joiner holds never returns.
|
|
#
|
|
# The window is not theoretical: entry->addr stays NULL for
|
|
# the whole resolution, so the fail-timeout arm fires after
|
|
# 30s - and getaddrinfo() against a blackholed resolver
|
|
# routinely blocks longer. The entry looks expired precisely
|
|
# while its own resolver is still running, hanging the task
|
|
# thread (and in unthreaded builds the frontend) over nothing
|
|
# worse than a slow DNS server.
|
|
#
|
|
# Reproducing that honestly would mean a 30s wait against an
|
|
# unreachable nameserver, so the test wraps two symbols at
|
|
# link time instead: getaddrinfo, to park the resolver
|
|
# mid-flight on one nominated hostname, and
|
|
# cpu_features_get_time_usec, to age the entry past the
|
|
# timeout instantly. --wrap rather than a strong definition
|
|
# because both live in libretro-common objects linked into
|
|
# the test.
|
|
#
|
|
# Because the failure mode is a hang rather than a wrong
|
|
# answer, the sweep runs behind a watchdog: on timeout the
|
|
# test reports and exits non-zero instead of blocking CI.
|
|
make -f Makefile.dns_evict clean SANITIZER=address && make -f Makefile.dns_evict -j$(nproc) all SANITIZER=address
|
|
ASAN_OPTIONS=detect_leaks=1 timeout 300 \
|
|
./http_dns_evict_deadlock_test
|
|
echo "[pass] http_dns_evict_deadlock_test (ASan)"
|
|
make -f Makefile.dns_evict clean SANITIZER=thread && make -f Makefile.dns_evict -j$(nproc) all SANITIZER=thread
|
|
TSAN_OPTIONS=halt_on_error=1 timeout 300 \
|
|
./http_dns_evict_deadlock_test
|
|
make -f Makefile.dns_evict clean
|
|
echo "[pass] http_dns_evict_deadlock_test (TSan)"
|
|
|
|
- name: ccache statistics
|
|
if: ${{ !cancelled() }}
|
|
continue-on-error: true
|
|
run: ccache -sv
|
|
|
|
- name: Save ccache
|
|
# Only master's pushes keep a cache: pull requests restore
|
|
# master's, so one push does not evict the next one's.
|
|
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/master'
|
|
uses: actions/cache/save@v4
|
|
with:
|
|
path: ~/.ccache
|
|
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
|