Skip to content

feat(p3-342): FluxEngine schreibt einen echten SCP-Behaelter - und dr… #897

feat(p3-342): FluxEngine schreibt einen echten SCP-Behaelter - und dr…

feat(p3-342): FluxEngine schreibt einen echten SCP-Behaelter - und dr… #897

Workflow file for this run

# ============================================================================
# UnifiedFloppyTool - Sanitizer Checks
#
# Runs ASan and UBSan builds to catch memory errors and undefined behaviour.
# Uses the CMake build path (library targets + test suite).
#
# Fixes applied:
# FAILURE-007: removed --target all, use default CMake target
# Added: ctest --build-config for multi-config generators
# ============================================================================
name: Sanitizer Checks
on:
push:
branches: [main, develop]
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
QT_VERSION: '6.10.1'
jobs:
asan:
# MF-432: without this GitHub's default is 360 min. On 2026-08-19
# five Linux jobs hung in the Qt/apt download and would have burned
# six hours each. A hang now dies here instead.
timeout-minutes: 60
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
modules: 'qtserialport'
cache: true
- name: Install dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y libusb-1.0-0-dev libgl1-mesa-dev
- name: Build with ASan (CMake)
# FIX-007: no --target all
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DUFT_BUILD_TESTS=ON \
-DUFT_WITH_DTC_COMPONENTS=ON \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build --parallel $(nproc) 2>&1 || true
# GUI may fail (missing headers) — tests are what matters
# ─────────────────────────────────────────────────────────────────
# SCHARF (MF-517). Dieser Schritt hat KEIN `|| true`.
#
# Er faehrt genau die Tests, die den Weg gehen, den ein Benutzer
# geht: eine Datei oeffnen. Dort lagen fuenf Speicherfehler, die
# 241 gruene Tests nicht gefunden haben (KNOWN_ISSUES FUZZ-1/FUZZ-2)
# — Heap-Ueberlauf 44830 B, Stapel-Ueberlauf 1800 B, ein doppeltes
# free bei jeder abgeschnittenen DSK-Datei, ein Lesen 29 KB hinter
# ein Feld, und zwoelf read_track, die durch NULL schrieben.
#
# detect_leaks=0 mit Absicht: geleckt wird in diesem Baum an vielen
# Stellen, und ein ungemessener Rueckstand darf kein Tor bewachen.
# Speicher-FEHLER sind etwas anderes als Speicher-Verlust; die
# ersten werden hier scharf geprueft, die zweiten weiter unten
# gemeldet.
# ─────────────────────────────────────────────────────────────────
# MF-1110 — VOR dem Tor zaehlen, wie viele Prueflinge es ueberhaupt
# hat. `--no-tests=error` unten feuert NUR, wenn die Regex GAR
# KEINEN Test trifft. Faellt einer von neun weg — umbenannt, in
# EXCLUDED_TESTS gewandert, oder schlicht nicht uebersetzt, weil
# der Bauschritt darueber `|| true` traegt —, dann laeuft das Tor
# mit ACHT und meldet GRUEN. Niemand zaehlt.
#
# Die Sollzahl wird aus der Regex ABGELEITET (Zahl der
# `|`-Alternativen), nicht danebengeschrieben: eine zweite
# gepflegte Zahl waere genau die Drift, die das hier verhindern
# soll (MF-1077). Die statische Haelfte — existieren die Namen
# ueberhaupt noch? — haelt `scripts/audit_tor_abdeckung.py`.
- name: Gate coverage — all named tests present (ARMED)
run: |
cd build
REGEX="test_convert_roundtrip_measured|test_convert_identity_lossless|test_track_add_sector_owns_copies|test_convert_fuzz|test_convert_roundtrip_lossless|test_disk_open_fuzz|test_disk_write_fuzz|test_dsk_open_bounds|test_format_probe_fuzz"
SOLL=$(printf '%s' "$REGEX" | tr '|' '\n' | grep -c .)
IST=$(ctest -N --tests-regex "$REGEX" | grep -cE '^[[:space:]]+Test[[:space:]]+#') || true
echo "scharfes ASan-Tor: $IST von $SOLL Prueflingen registriert"
if [ "$IST" -ne "$SOLL" ]; then
echo "::error::Das scharfe Tor haette mit $IST statt $SOLL Tests laufen und GRUEN melden koennen."
echo "Registriert sind:"
ctest -N --tests-regex "$REGEX" | grep -E '^[[:space:]]+Test[[:space:]]+#' || true
exit 1
fi
- name: Memory-safety gate — open path (ARMED)
run: |
cd build
ctest --output-on-failure --timeout 300 --no-tests=error \
--tests-regex "test_convert_roundtrip_measured|test_convert_identity_lossless|test_track_add_sector_owns_copies|test_convert_fuzz|test_convert_roundtrip_lossless|test_disk_open_fuzz|test_disk_write_fuzz|test_dsk_open_bounds|test_format_probe_fuzz"
env:
# halt_on_error=0: EIN Lauf meldet ALLE Funde statt beim ersten
# abzubrechen. Sonst kostet jeder einzelne Fehler eine eigene
# CI-Runde — beim ersten scharfen Lauf (MF-520) war genau das
# der Fall, und hinter dem gemeldeten koennen weitere stehen.
# ASan setzt den Exit-Code trotzdem, das Tor bleibt scharf.
ASAN_OPTIONS: detect_leaks=0:halt_on_error=0:exitcode=1:print_stacktrace=1
# ─────────────────────────────────────────────────────────────────
# BERICHTEND, nicht scharf.
#
# Bis MF-517 lief hier eine Liste von NEUN Testnamen, und das mit
# `|| true`. Damit sah der Baum 232 seiner 241 Tests nie unter ASan,
# und die neun, die er sah, konnten nichts roeten. Als "ASan-
# Workflow" gefuehrt zu werden und beides zu sein, ist genau die
# Sorte Zusage, gegen die DESIGN_PRINCIPLES geschrieben ist.
#
# Jetzt laeuft die ganze Suite, damit der Rueckstand GEMESSEN im Log
# steht statt geschaetzt zu werden. `|| true` bleibt hier bewusst:
# den Rueckstand scharf zu schalten, ohne ihn vorher zu kennen, waere
# derselbe Fehler in die andere Richtung.
- name: Full suite under ASan (reporting only, incl. leaks)
# MF-530: `if: always()`, weil GitHub den Auftrag sonst
# abbricht, sobald das scharfe Tor darueber rot wird — und
# dann faellt genau die Messung aus, die den Rueckstand
# sichtbar haelt. Beim d80-Fund (MF-529) ist das passiert:
# ein Befund im Tor, und der Leck-Bericht des ganzen Laufs
# war weg. Ein Tor, das die Messung mitreisst, misst nicht
# mehr.
if: always()
run: |
cd build
ctest --output-on-failure --timeout 300 || true
env:
ASAN_OPTIONS: detect_leaks=1:abort_on_error=1:print_stacktrace=1
- name: Upload ASan findings
if: failure()
uses: actions/upload-artifact@v4
with:
name: asan-findings-${{ github.sha }}
path: |
build/Testing/Temporary/
build/**/*.log
retention-days: 7
ubsan:
# MF-432: without this GitHub's default is 360 min. On 2026-08-19
# five Linux jobs hung in the Qt/apt download and would have burned
# six hours each. A hang now dies here instead.
timeout-minutes: 60
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: linux
target: desktop
modules: 'qtserialport'
cache: true
- name: Install dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y libusb-1.0-0-dev libgl1-mesa-dev
- name: Build with UBSan (CMake)
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DUFT_BUILD_TESTS=ON \
-DUFT_WITH_DTC_COMPONENTS=ON \
-DCMAKE_C_FLAGS="-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \
-DCMAKE_CXX_FLAGS="-fsanitize=undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined"
cmake --build build --parallel $(nproc) 2>&1 || true
# GUI may fail (missing headers) — tests are what matters
# MF-1110 — dieselbe Zaehlung wie im ASan-Auftrag; die Begruendung
# steht dort. Sie gehoert in BEIDE Auftraege, weil jeder seinen
# eigenen Bau hat und ein Ziel im einen uebersetzen kann und im
# anderen nicht.
- name: Gate coverage — all named tests present (ARMED)
run: |
cd build
REGEX="test_convert_roundtrip_measured|test_convert_identity_lossless|test_track_add_sector_owns_copies|test_convert_fuzz|test_convert_roundtrip_lossless|test_disk_open_fuzz|test_disk_write_fuzz|test_dsk_open_bounds|test_format_probe_fuzz"
SOLL=$(printf '%s' "$REGEX" | tr '|' '\n' | grep -c .)
IST=$(ctest -N --tests-regex "$REGEX" | grep -cE '^[[:space:]]+Test[[:space:]]+#') || true
echo "scharfes UBSan-Tor: $IST von $SOLL Prueflingen registriert"
if [ "$IST" -ne "$SOLL" ]; then
echo "::error::Das scharfe Tor haette mit $IST statt $SOLL Tests laufen und GRUEN melden koennen."
echo "Registriert sind:"
ctest -N --tests-regex "$REGEX" | grep -E '^[[:space:]]+Test[[:space:]]+#' || true
exit 1
fi
# SCHARF (MF-517) — siehe die Begruendung im ASan-Auftrag oben.
- name: UB gate — open path (ARMED)
run: |
cd build
ctest --output-on-failure --timeout 300 --no-tests=error \
--tests-regex "test_convert_roundtrip_measured|test_convert_identity_lossless|test_track_add_sector_owns_copies|test_convert_fuzz|test_convert_roundtrip_lossless|test_disk_open_fuzz|test_disk_write_fuzz|test_dsk_open_bounds|test_format_probe_fuzz"
env:
# MF-1102 — hier stand `halt_on_error=0` mit der Begruendung
# "wie beim ASan-Tor: ein Lauf, alle Funde". Die Begruendung
# stimmt fuer ASan und NICHT fuer UBSan, und der Unterschied
# ist gemessen (WSL2, gcc 15.2.0, Rotbeweis mit einem
# signed-integer-overflow):
#
# UBSan, genau diese Zeile -> Exit 0, 524 Byte Befund
# dieselbe Zeile, halt_on_error=1 -> Exit 1
# -fno-sanitize-recover=all -> Exit 1, auch ohne Option
#
# ASan-Fehler sind von Haus aus nicht wiederaufsetzbar und
# beenden den Prozess ohnehin (gemessen: Exit 1 auch voellig
# ohne ASAN_OPTIONS). UBSan setzt dagegen wieder auf, meldet
# und laeuft weiter — `exitcode` greift nur, wenn es ueberhaupt
# abbricht. Dieses Tor konnte damit NICHT rot werden.
#
# Die Konfiguration war zudem VERTAUSCHT: der gatende Schritt
# hier trug den milden Wert, der berichtende Schritt darunter
# (mit `|| true`) den strengen. Der strenge stand dort, wo er
# nichts bewirkt.
#
# Jetzt doppelt gehalten: das Uebersetzungs-Flag oben laesst
# UBSan gar nicht erst wieder aufsetzen, und die Laufzeitoption
# sagt dasselbe noch einmal — damit haengt das Tor nicht an
# einer Umgebungsvariablen, die jemand streicht. Der Preis ist
# benannt: ein Lauf zeigt nur noch den ERSTEN Fund. Die
# vollstaendige Liste liefert der berichtende Schritt darunter,
# und der ist genau dafuer da.
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1:exitcode=1
# BERICHTEND. Vorher liefen hier neun Testnamen mit `|| true`; die
# restlichen 232 sah niemand unter UBSan.
- name: Full suite under UBSan (reporting only)
# MF-530: `if: always()`, weil GitHub den Auftrag sonst
# abbricht, sobald das scharfe Tor darueber rot wird — und
# dann faellt genau die Messung aus, die den Rueckstand
# sichtbar haelt. Beim d80-Fund (MF-529) ist das passiert:
# ein Befund im Tor, und der Leck-Bericht des ganzen Laufs
# war weg. Ein Tor, das die Messung mitreisst, misst nicht
# mehr.
if: always()
run: |
cd build
ctest --output-on-failure --timeout 300 || true
env:
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
- name: Upload UBSan findings
if: failure()
uses: actions/upload-artifact@v4
with:
name: ubsan-findings-${{ github.sha }}
path: |
build/Testing/Temporary/
build/**/*.log
retention-days: 7