A hibajavítás akkor válik felülvizsgálható mérnöki eredménnyé, ha más is meg tudja állapítani, mi váltotta ki a hibát, mely szerződés sérült, hogyan szünteti meg ezt a változtatás, és mit ellenőriztek ténylegesen. A „nálam már nem omlik össze” megfigyelés ehhez kevés: eltűnhetett a kiváltó körülmény anélkül, hogy a hiba megszűnt volna.
A C biztonsági hibaminták és védekezés a megsérthető szerződéseket, az ASan, UBSan, MSan, TSan és Valgrind az eszközök megfigyelési határait, az Unit test, property test és fuzzing pedig a tesztbemenetek előállítását tárgyalta. Most ezekből állítunk össze reprodukálható ellenőrzési ciklust.
A minőségi kapu (quality gate) előre meghatározott elfogadási feltétel, amely egy azonosítható forrásállapotra, rögzített környezetben végzett ellenőrzések eredményét értékeli. Nem pusztán egy CI-job neve: tartalmazza a szükséges konfigurációt, a siker feltételeit és a megőrzendő bizonyítékokat.
Például:
A változás akkor fogadható el, ha a kétfordítós build új diagnosztika nélkül elkészül, minden kötelező teszt sikeres, és nincs kivizsgálatlan sanitizer- vagy statikuselemző-jelzés.
A „sikeres” jelentését is rögzíteni kell. A kihagyott vagy az infrastruktúra hibája miatt el sem indult teszt nem sikeres teszt. A támogatás hiánya külön állapot, amelyhez indoklás és jóváhagyott kivétel tartozik.
Az ellenőrzési ciklus:
A regressziós tesztnek lehetőség szerint a hibás változatot el kell utasítania, a javítottat pedig el kell fogadnia. Enélkül nem világos, hogy valóban a javítandó tulajdonságot ellenőrzi-e.
ISO C17: a példakód szabványos C17-et használ. Eszközprofil: az alábbi laborhoz kijelölt környezet Linux x86-64, System V ABI, LP64, GCC 14.2.0, LLVM/Clang és clang-tidy 18.1.8, glibc 2.39. A sanitizer-runtime a megfelelő fordítói telepítéshez tartozzon. Ez kijelölt, nem itt kipróbált profil; a parancsok nem futtatási eredmények.
A gcc-14 és clang-18 programnév önmagában
nem rögzíti a javítóverziót. A környezetet csomagverzióval vagy
konténerképdigesttel is azonosítani kell.
A GCC és a Clang eltérő diagnosztikát és optimalizálási döntéseket adhat. A két build ezért hasznos keresztellenőrzés, de nem két független helyességbizonyítás.
| Profil | Fordítók | Cél |
|---|---|---|
| Debug | GCC és Clang | Könnyen vizsgálható állapot, regressziós tesztek |
| Release | GCC és Clang | Optimalizált és NDEBUG-os viselkedés ellenőrzése |
| ASan + UBSan | GCC és Clang | Instrumentált memória- és kiválasztott UB-ellenőrzések |
| TSan | Külön Clang-build | Konkurens tesztek vizsgálata |
Fordítóeszköz-szabály: a TSan-profilt külön kell tartani az ASan-profiltól; nem minden sanitizer kombinálható. Minden profilt saját tárgyfájlokból kell felépíteni, és a szükséges instrumentációs kapcsolókat a végső linkelésnél is meg kell adni.
A warning policy legyen szigorú, de kezelhető. Saját kódban célszerű
a kijelölt warningokat hibaként kezelni. Külső függőségekre külön
szabályzat vonatkozhat. Elhallgattatás csak szűk hatókörben, indoklással
és felülvizsgálható kivétellel történjen. Fordítófrissítéskor az új
diagnosztikákat külön migrációban érdemes rendezni, nem a
-Werror tartós kikapcsolásával.
Egy objektum két puffert birtokol. A hiba: a második allokáció sikertelenségekor az első puffer felszabadítása elmarad. A normál sikeres teszt ezt nem gyakorolja.
A hiba-injektálás (failure injection) itt az adott sorszámú allokáció determinisztikus meghiúsítása. A regressziós teszt a sikeres eset mellett mindkét hibapontot ellenőrzi.
A pair_init előfeltétele, hogy a kimeneti objektum üres
legyen. Siker esetén két puffer tulajdonosa lesz; hiba esetén üres
marad. A függvény 0 értékkel jelez sikert, -1
értékkel allokációs hibát.
| Objektum vagy hozzáférés | Tulajdonos és élettartam | Érvénytelenítés |
|---|---|---|
| Átmeneti pufferek | Inicializálás közben a helyi tmp kezeli őket |
Hibás inicializáláskor felszabadítás |
| Sikeresen átadott pufferek | A hívó p objektuma birtokolja őket |
pair_destroy |
out, a, p
paraméterpointer |
Kölcsönzött hozzáférés a hívó élő objektumaihoz; nincs eltárolás | A hívó objektumának élettartamvége |
| Számlálóállapot | A teszt aktuális iterációjának helyi objektuma | Az iteráció vége |
A tulajdonos objektum élettartamának vége C-ben nem szabadítja fel
automatikusan a dinamikus tárolót: a pair_destroy hívás a
szerződés része.
Az alábbi teljes gate.c programban a méretek C bájtban
értendők. Nincs konkrét objektumlayoutra vonatkozó feltételezés.
#include <stddef.h>
#include <stdlib.h>
struct alloc_state {
size_t calls;
size_t fail_at; /* 0: nincs injektált hiba */
size_t live;
};
struct pair {
void *first;
void *second;
};
static void *tracked_alloc(struct alloc_state *a, size_t bytes)
{
++a->calls;
if (a->fail_at != 0 && a->calls == a->fail_at)
return NULL;
void *p = malloc(bytes);
if (p != NULL)
++a->live;
return p;
}
static void tracked_free(struct alloc_state *a, void *p)
{
if (p != NULL) {
free(p);
--a->live;
}
}
static int pair_init(struct pair *out, struct alloc_state *a)
{
struct pair tmp = {0};
tmp.first = tracked_alloc(a, 8);
if (tmp.first == NULL)
return -1;
tmp.second = tracked_alloc(a, 16);
if (tmp.second == NULL) {
tracked_free(a, tmp.first);
return -1;
}
*out = tmp; /* Logikai tulajdonátadás. */
return 0;
}
static void pair_destroy(struct pair *p, struct alloc_state *a)
{
tracked_free(a, p->second);
tracked_free(a, p->first);
*p = (struct pair){0};
}
int main(void)
{
for (size_t fail = 0; fail <= 2; ++fail) {
struct alloc_state a = {0, fail, 0};
struct pair p = {0};
int rc = pair_init(&p, &a);
if (rc != (fail == 0 ? 0 : -1))
return EXIT_FAILURE;
if (a.calls != (fail == 1 ? 1u : 2u))
return EXIT_FAILURE;
if (fail == 0) {
if (p.first == NULL || p.second == NULL || a.live != 2)
return EXIT_FAILURE;
} else {
if (p.first != NULL || p.second != NULL || a.live != 0)
return EXIT_FAILURE;
}
pair_destroy(&p, &a);
if (p.first != NULL || p.second != NULL || a.live != 0)
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}A teszt feltételezi, hogy a nem injektált apró allokációk sikerülnek.
Valós memóriahiány esetén nem nulla kilépési állapot keletkezhet; annak
eredetét külön kell kivizsgálni. Az ellenőrzések szándékosan nem
assert hívások: release profilban, NDEBUG
mellett is megmaradnak.
A következő Bash-parancsok fordítanak, de nem indítják el a tesztprogramokat:
common=(-std=c17 -Wall -Wextra -Wpedantic -Wconversion
-Wshadow -Wstrict-prototypes -Wmissing-prototypes
-Wformat=2 -Werror)
gcc-14 "${common[@]}" -O0 -g3 gate.c -o gate-gcc-debug
clang-18 "${common[@]}" -O0 -g3 gate.c -o gate-clang-debug
gcc-14 "${common[@]}" -O2 -DNDEBUG gate.c -o gate-gcc-release
clang-18 "${common[@]}" -O2 -DNDEBUG gate.c -o gate-clang-release
gcc-14 "${common[@]}" -O1 -g -fno-omit-frame-pointer \
-fsanitize=address,undefined -fno-sanitize-recover=all \
gate.c -o gate-gcc-asan-ubsan
clang-18 "${common[@]}" -O1 -g -fno-omit-frame-pointer \
-fsanitize=address,undefined -fno-sanitize-recover=all \
gate.c -o gate-clang-asan-ubsan
clang-18 "${common[@]}" -O1 -g -fno-omit-frame-pointer \
-fsanitize=thread gate.c -o gate-clang-tsan
gcc-14 "${common[@]}" -fanalyzer -c gate.c -o gate-analyzer.o
clang-tidy-18 \
--checks='-*,clang-analyzer-*,bugprone-sizeof-expression' \
gate.c -- -std=c17Eszközfüggő feltételek: a GCC statikus elemzőjének elérhetősége a telepítéstől is függhet; a clang-tidy külön csomag lehet. Az ellenőrzéskészletet az adott verzióhoz kell rögzíteni. C projekthez ne kapcsoljunk be válogatás nélkül C++-specifikus modernizáló szabályokat.
Többfájlos projektben az elemző ugyanazokat az include-útvonalakat, makrókat és targetbeállításokat kapja, mint a build; ezt célszerű fordítási adatbázissal biztosítani. A clang-tidy diagnosztikája önmagában nem feltétlenül jelent sikertelen CI-jobot: az elfogadási szabályt és a kilépési állapot kezelését külön konfigurálni kell.
A példa egyszálú. TSan-buildje legfeljebb az instrumentált környezet próbája, nem konkurenciateszt. A valódi TSan-kapuhoz konkurens tesztterhelés szükséges.
Tulajdonátadás nélküli vizsgálati tartományban legyen a még lezárandó erőforrások száma, a sikeres szerzéseké, az érvényes elengedéseké. Ekkor:
A második allokáció injektált hibájánál kezdetben , majd egy sikeres szerzés történik: . A javított visszagörgetés egyszer elenged: , így . A hibás ágban , ezért egy kötelezettség megmarad.
Bizonyításvázlat: minden sikeres szerzés eggyel növeli, minden hozzá tartozó érvényes elengedés eggyel csökkenti a mérleget; a műveletsor szerinti indukció adja az összefüggést. Feltétel, hogy minden műveletet számoljunk, és egy szerzéshez legfeljebb egy elengedés tartozzon.
A nulla számláló önmagában nem elég: hibás könyvelés, számlálatlan allokáció vagy egymást számszerűen elfedő hibák mellett félrevezető lehet. File descriptorokra és más erőforrásokra külön, azonosságot is követő nyilvántartás szükséges.
Egy review-zható mintajegyzőkönyv:
| Mező | Tartalom |
|---|---|
| Trigger | A második allokáció sikertelen |
| Megsértett szerződés | Sikertelen inicializálás után nem maradhat birtokolt puffer |
| Root cause | A részleges sikerhez nem tartozott visszagörgetés |
| Változás | Az első puffer felszabadítása a második allokáció hibájánál |
| Regresszió | fail_at = 2; elvárt üres kimenet és nulla élő
allokáció |
| Ellenőrzés | Mindkét fordító, debug/release, statikus elemzés, ASan/UBSan |
| Nyitott korlát | A teszt nem általános bizonyíték minden hívóra vagy erőforrásra |
Az itt felsorolt eredmények elvárások, nem közölt mérések. A kitöltött jegyzőkönyvnek a tényleges kilépési állapotot és naplókat kell tartalmaznia.
A bizonyíték eredetkövetése (provenance) legalább a következőket kapcsolja össze:
A szabványos nyelvi állítást, az eszköz dokumentált képességét és a saját mérési megfigyelést külön kell kezelni. Csak ténylegesen elolvasott forrásra hivatkozzunk; keresési kiindulópont nem bizonyíték. Ellenőrzött forrásjegyzet hiányában a dokumentációs ellenőrzés nyitott tétel marad.
Idő- vagy memóriaköltség mérésénél a terhelést és az ismétléseket is rögzíteni kell. Sanitizeres időeredmény nem azonos a release program teljesítményével.
A zöld CI összetévesztése a helyességgel a legsúlyosabb értelmezési hiba. A zöld állapot csak a kijelölt ellenőrzések eredménye; a nem vizsgált bemenetek és ütemezések nyitva maradnak.
Szintén gyakori, hogy a teszt csak a visszatérési kódot nézi, miközben a javítandó hiba erőforrásszivárgás. A regresszióban ezért a hibakód mellett a kimeneti állapot és az erőforrásmérleg is ellenőrzendő.
Végül a globális diagnosztika-elhallgattatás látszólag helyreállítja a kaput, de megszünteti annak információértékét. Kivételhez konkrét jelzés, indoklás, felelős és felülvizsgálati feltétel tartozzon.
pair_destroy kétszer is meghívható
ugyanarra az objektumra.A futás csak a végrehajtott útvonalakat vizsgálja, az eszközök pedig meghatározott hibakategóriákat keresnek. Például egy lezáratlan file descriptor nem általános ASan/UBSan-célpont. Az elfogadható válasz megnevezi mind a lefedettségi, mind az eszközérzékenységi korlátot, és külön erőforrásellenőrzést javasol.
Az első destrukció utáni sikeres ellenőrzést követően illeszd be ezt a C17-részletet:
pair_destroy(&p, &a);
if (p.first != NULL || p.second != NULL || a.live != 0)
return EXIT_FAILURE;Az első hívás nullázza a pointereket, ezért a második nem enged el újabb erőforrást. Az ellenőrzési feltétel: minden injektálási esetben nulla mérleg és üres objektum maradjon, runtime-diagnosztika nélkül.
Először azonos forrást, bemenetet és pontos buildparancsokat kell
kérni. Külön vizsgálandó az optimalizálás és a NDEBUG
hatása; különösen az, hogy került-e szükséges mellékhatás
assert kifejezésbe. Ezután minimális reprodukció, a
C-szerződések ellenőrzése és második fordítós összehasonlítás
következik. Elfogadás csak megindokolt root cause és a release profilban
is aktív regressziós teszt alapján történhet.
A jó minőségi kapu nem hibamentességet ígér, hanem reprodukálható ellenőrzést. A kétfordítós build, a C-hez illesztett statikus elemzés, az elkülönített runtime-profilok és a hiba-injektálás egymást kiegészítő bizonyítékot adnak. A hibajavítási jegyzőkönyv ezt összekapcsolja a kiváltó okkal, a változással és a regresszióval — és ugyanilyen világosan megnevezi, mi maradt bizonyítatlan.