Repository navigation
2276 lines (2187 loc) · 118 KB
/
Copy pathLinux-samples-tasks.yml
File metadata and controls
2276 lines (2187 loc) · 118 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: CI Linux samples/tasks
on:
push:
branches:
- master
pull_request:
branches:
- master
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)"