14.4 Biztonságos assembly- és C-interop gyakorlat

14.4.1 Motiváció: a határ a leggyengébb láncszem

Az Assembly függvény hívása C-ből és C hívása assemblyből alfejezet egyik fő megállapítása az volt, hogy a sikeres linkelés semmit nem ellenőriz. A C fordító a lab_kernel(p, n) híváskor legfeljebb annyit tud, amit a fejlécbeli prototípusból kiolvas: a pointer és a hossz futásidejű értékéről, a fogadó fél tényleges kapacitásáról, a pufferek átfedéséről nem tud semmit. Amint a vezérlés átlép a saját kezűleg írt assembly modulba, megszűnik a típusrendszer, a Fortify, az automatikus konverzió, sőt a -Wall is.

A verem-túlcsordulás mechanizmusa assembly szinten alfejezet pontosan ezt a rést használta ki demonstrációs célból: egy strb ciklus, amely egy fix méretű keretbeli puffert írt, miközben a hossz a hívótól, ellenőrizetlenül érkezett. A ROP-lánc felismerése és miértje pedig megmutatta, mi lesz ebből a következmény: a felülírt mentett visszatérési cím kontrolláram-eltérítési primitív.

A mérnöki feladat ebben az alfejezetben tehát nem az, hogy felismerjük a hibát (azt már megtettük), hanem hogy megtervezzük a határt, ahol a hibát meg lehet fogni: a hívó és a hívott közötti szerződést rögzítjük, és a hívott fél a saját kapacitásához köti a kapott hosszt, mielőtt egyetlen bájtot is írna.

14.4.2 Definíció: az interop-határ biztonsági szerződése

Definíció. A C–assembly interop-határ szerződése négy dimenzió együttes rögzítése, amelyet a két oldalnak azonos módon kell értelmeznie:

  1. értékhatomány — minden argumentum megengedett értékkészlete (méret, index, kapacitás, kapcsoló bit);
  2. pointer-érvényesség — mit jelent az, hogy egy pointer használható (nem-NULL, olvasható, elég hosszú, nem fed át a céllal);
  3. tulajdonjog és élettartam — ki írja és ki olvassa a memóriát, meddig él a puffer;
  4. hibajelzési konvenció — hogyan jelzi a hívott fél, hogy a szerződés megsértése miatt nem hajtotta végre a műveletet.

A négy dimenzióból az első kettő az, amit a legtöbbször elhanyagolunk. Fontos mérnöki belátás: a pointer érvényessége általánosan nem eldönthető C-ben. A p != NULL csak a legdurvább esetet fogja meg; egy 0x1000-szerű, nem leképezett cím éppúgy átcsúszik, mint egy olyan, amely a szomszéd tömbbe mutat. Ezért a működő stratégia nem az, hogy „megvizsgáljuk, érvényes-e a pointer”, hanem hogy a kapott hosszt a fogadó fél saját, fordítási időben ismert kapacitásához kötjük. Ez az az ellenőrzés, amely olcsó, hordozható és determinisztikus.

Ellenőrzés Hol végezhető Költség Mit nem fog meg
NULL-ellenőrzés határon, 1 regiszterteszt ~1 utasítás érvénytelen, de nem-NULL pointert
hossz felső korlát határon, 1 összehasonlítás ~2 utasítás hibás, de a korláton belüli hosszt
pointer-tartomány (linker szimbólumok, pl. __bss_start, _end) futásidőben, platformfüggő több tíz ciklus nem hordozható, nem teljes
ASan / hardveres címcímkézés teszt- vagy speciális build 2–10× lassulás éles buildben nincs jelen

14.4.3 Kidolgozott példa: határellenőrzött másoló

Munkakönyvtár: ~/arm-labor/a64/a056/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).

mkdir -p ~/arm-labor/a64/a056
cd ~/arm-labor/a64/a056

A cél egy olyan modul, amely egy 64 bájtos belső puffert tart karban, és egy (src, n) párt fogad el. A szerződés teljes alakja:

Szerződési elem Kinek a kötelezettsége Ellenőrzi-e a modul?
src != NULL hívó igen (cbz)
n ≤ 63 (a lezáró '\0' helye is kell) hívó igen (cmp/b.hi)
src[0 … n−1] olvasható, azaz legalább n érvényes forrásbájt hívó nem – általánosan nem is ellenőrizhető
a forrás nem fed át a belső puffer írt tartományával ([0 … n]); megengedett átfedés nincs, így a lab_buffer() mutatója sem adható át forrásként hívó nem
a belső puffer egyetlen, közös, statikus példány; a modul nem reentráns és nem szálbiztos, párhuzamos (vagy jelkezelőből érkező) hívásokat a hívónak kell sorosítania hívó nem
siker: visszatérés n, a pufferben n bájt és egy '\0' modul –
elutasítás (src == NULL vagy n > 63): visszatérés -1, a puffer változatlan modul –

A lab_buffer() által visszaadott mutató a modul belső tárára mutat, nem önálló élettartamú eredményobjektum: a mutató a program teljes futása alatt érvényes, a tartalom viszont csak a következő sikeres lab_copy_bounded hívásig. Akinek tartós eredmény kell, a hívás után átmásolja. A hosszellenőrzés ezen semmit nem változtat: egy jól ellenőrzött hossz sem teszi a közös puffert használó függvényt reentránssá.

A „mindent vagy semmit” garancia tehát a szerződés alapján elutasított argumentumokra vonatkozik (NULL, túl nagy n). Ha a hívó megszegi a nem ellenőrzött feltételeket (például n megengedett, de a forrásból csak 10 bájt olvasható), a másolás közben memóriahiba keletkezhet, és ekkor a puffer félig felülírt állapotban maradhat. Erre a modul nem ad és nem is adhat garanciát.

A teszthajtó minden esetnél a hívás előtt és után is lemásolja a puffert. Elutasításkor azt ellenőrzi, hogy a tartalom bájtra azonos maradt, sikernél pedig a másolt bájtokat és a lezáró nullát. Az esetek a határ két oldalát (62, 63, 64) és a konverziós szélsőértéket (SIZE_MAX) is lefedik:

/* lab_a056.c – biztonságos interop-határ demonstráció (A64 ISA) */
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>

#define LAB_CAP 64u

/* Szerződés (a teljes szöveget lásd a fejezetben):
 *  - src != NULL, és src[0..n-1] olvasható (legalább n bájt);
 *  - n <= LAB_CAP-1 (63);
 *  - a forrás nem fedhet át a belső pufferrel (src nem lehet lab_buffer()-ből);
 *  - a belső puffer egyetlen, közös példány: a függvény nem reentráns,
 *    párhuzamos hívásokat a hívónak kell sorosítania.
 * Visszatérés: n siker esetén (a puffer: n bájt + '\0');
 *              -1, ha src == NULL vagy n > 63 – ekkor a puffer érintetlen. */
long           lab_copy_bounded(const uint8_t *src, size_t n);
const uint8_t *lab_buffer(void);   /* csak megfigyeléshez; a tartalom a
                                      következő sikeres hívásig érvényes */

static uint8_t src[4096];

static int check(const char *name, const uint8_t *p, size_t n, long want)
{
    uint8_t before[LAB_CAP], after[LAB_CAP];
    memcpy(before, lab_buffer(), LAB_CAP);
    long got = lab_copy_bounded(p, n);
    memcpy(after, lab_buffer(), LAB_CAP);

    int ok = (got == want);
    if (want < 0)                       /* elutasítás: nincs mellékhatás */
        ok = ok && memcmp(before, after, LAB_CAP) == 0;
    else                                /* siker: n bájt + lezáró nulla  */
        ok = ok && memcmp(after, p, n) == 0 && after[n] == 0;
    printf("%-16s -> %3ld (várt %3ld) %s\n",
           name, got, want, ok ? "OK" : "HIBA");
    return ok;
}

int main(void)
{
    int ok = 1;
    memset(src, 'A', sizeof(src));

    ok &= check("n=5",        src, 5,          5);
    ok &= check("n=62",       src, 62,        62);
    ok &= check("n=63",       src, 63,        63);
    ok &= check("n=64",       src, 64,        -1);
    ok &= check("NULL, n=5",  NULL, 5,        -1);
    ok &= check("n=SIZE_MAX", src, SIZE_MAX, -1);
    printf("puffer hossza: %zu\n", strlen((const char *)lab_buffer()));
    return ok ? 0 : 1;
}

Az assembly oldal (A64 ISA, little-endian):

/* lab_bound.S – határellenőrzött másoló (A64 ISA) */
    .arch armv8-a
    .equ    LAB_CAP,  64
    .equ    LAB_MAXN, LAB_CAP - 1        /* 63: a lezáró '\0' indexe is belefér */

    .bss
    .align  4
lab_buf:
    .skip   LAB_CAP
    .size   lab_buf, LAB_CAP

    .text
    .global lab_copy_bounded
    .type   lab_copy_bounded, %function
lab_copy_bounded:
    /* x0 = src, x1 = n  (A64 ISA) */
    cbz     x0, .Lreject                 /* pointer-érvényesség: nem-NULL  */
    mov     x2, #LAB_MAXN
    cmp     x1, x2
    b.hi    .Lreject                     /* n > 63 -> elutasítás, MÉG NEM ÍRTUNK */

    adrp    x3, lab_buf                  /* lapcím + 12 bites eltolás      */
    add     x3, x3, :lo12:lab_buf

    mov     x4, #0                       /* i = 0                          */
.Lloop:
    cmp     x4, x1
    b.hs    .Ldone
    ldrb    w5, [x0, x4]
    strb    w5, [x3, x4]
    add     x4, x4, #1
    b       .Lloop
.Ldone:
    strb    wzr, [x3, x1]                /* '\0' az n. indexre (n <= 63)   */
    mov     x0, x1                       /* siker: a másolt bájtok száma   */
    ret
.Lreject:
    mov     x0, #-1                      /* MOVN -> x0 = 0xFFFF_FFFF_FFFF_FFFF */
    ret
    .size   lab_copy_bounded, . - lab_copy_bounded

    .global lab_buffer
    .type   lab_buffer, %function
lab_buffer:
    adrp    x0, lab_buf
    add     x0, x0, :lo12:lab_buf
    ret
    .size   lab_buffer, . - lab_buffer

    .section .note.GNU-stack,"",%progbits   /* nem kér végrehajtható vermet */

Fordítás és futtatás (qemu-user target). A teszthajtó kilépési állapota 0, ha minden eset OK, egyébként 1:

aarch64-linux-gnu-gcc -O2 -Wall -Wextra -fstack-protector-strong -fPIE -pie \
    -o lab-a056 lab_a056.c lab_bound.S
qemu-aarch64 -L /usr/aarch64-linux-gnu ./lab-a056; echo "kilépési állapot: $?"

Kimenet a helyettesítő eszközlánccal (zig cc/Clang + musl, statikus build, Unicorn-alapú emulátor) mérve; a GNU-eszközlánccal és qemu-aarch64-gyel ugyanezt kell kapni, mert a kimenet csak a programlogikától függ:

n=5              ->   5 (várt   5) OK
n=62             ->  62 (várt  62) OK
n=63             ->  63 (várt  63) OK
n=64             ->  -1 (várt  -1) OK
NULL, n=5        ->  -1 (várt  -1) OK
n=SIZE_MAX       ->  -1 (várt  -1) OK
puffer hossza: 63
kilépési állapot: 0

A határ körüli három bemenet a helyes kódra és a két jellegzetes elrontott változatra (mindegyik ugyanabból a forrásból, egyetlen sor módosításával készült és lefutott):

Változat n = 62 n = 63 n = 64 A hiba jellege
helyes: cmp x1, #63 + b.hi 62 63 −1 –
b.hs a b.hi helyett (63-mal hasonlítva) 62 −1 −1 túl szigorú: az érvényes 63-at is elutasítja, memóriahibát nem okoz
cmp x1, #64 + b.hi (a kapacitással, nem a maximális hosszal hasonlítva) 62 63 64 túl engedékeny: a '\0' a 64. indexre, a puffer mögé kerül

Levezetés az indexhatárra. Tegyük fel, hogy az elutasítás után az n ≤ 63 feltétel fennáll. A ciklusváltozóra i ∈ {0, …, n−1}, így a cikluson belüli legnagyobb írási index n−1 ≤ 62. A lezáró bájt indexe n ≤ 63. A puffer érvényes indexei 0 … 63, tehát minden írás a pufferen belül marad. A feltétel tehát elégséges. A cmp x1, #63 után a b.hi a 63-nál nagyobb, előjel nélküli hosszt utasítja el: n = 63 még elfogadott, n = 64 már elutasított. b.hs-re cserélve a 63-as értéket is elutasítanánk. A lezáró nulla a belső 64 bájtos puffer n indexű bájtjába kerül. Elfogadási táblázat: n = 62 → siker (62); n = 63 → siker (63); n = 64 és n = SIZE_MAX → -1, a puffer bájtra változatlan. Mind a négy sort a fenti teszthajtó ellenőrzi.

Miért fontos a sorrend? Az elutasítási ág a másolás előtt fut le, ezért a szerződés alapján elutasított argumentumokra a modul „mindent vagy semmit” módon viselkedik: nem marad félig felülírt puffer. Ez a tulajdonság nem esztétikai kérdés — egy részleges írás a hívó oldalon konzisztensnek látszó, de tartalmilag hibás állapotot hagyhat maga után.

14.4.4 Tipikus hibák és ellenpróbák

1. mov w0, #-1 a mov x0, #-1 helyett. A W-regiszterbe írás a felső 32 bitet nullázza, így x0 = 0x0000_0000_FFFF_FFFF: a hívó long visszatérési értéke 4294967295 lesz, nem -1. A hívóoldali if (ret < 0) ellenőrzés csendben elmarad. Ellenpróba: objdump -d lab-a056 | grep -A2 reject, illetve a C oldalon printf("%ld"), nem %d.

2. A határellenőrzés a konverzió után történik-e. Ha a C hívó int-et ad át és a fogadó size_t-ként kapja, a negatív érték előjeles→előjel nélküli konverzióval hatalmas számmá válik. Ez nem hiba, ha az ellenőrzés az átvétel utáni értéken fut (nálunk így van: a cmp x1, x2 a nyers bitmintát vizsgálja). Ellenpróba: lab_copy_bounded(src, (size_t)-1) → várt -1.

3. Off-by-one a feltételkódban vagy a konstansban. A cmp x1, #63 után a b.hi az n > 63, a b.hs az n ≥ 63 esetet utasítja el. A b.hs tehát nem engedi át a 64-et, hanem túl szigorú: az érvényes n = 63-at is visszautasítja (a tesztben az n=63 sor -1-et ad, és a puffer hossza 62 marad). Ez funkcionális hiba, memóriát nem ír felül. A veszélyes off-by-one a másik irány: ha a kódot a kapacitással (cmp x1, #64) hasonlítjuk a maximális hossz helyett, a b.hi átengedi a 64-et, és a strb wzr, [x3, x1] a 64. indexre ír, egy bájttal a puffer mögé. Ez az a022 óta ismert [Xn, Xm] címzési mód klasszikus csapdája. Ellenpróba: a fenti táblázat három sora; az n=63 sornak 63-at, az n=64 sornak -1-et kell adnia.

4. Hiányos clobber-lista inline assemblynél. Ha ugyanezt a logikát C asm volatile blokkban írnánk meg (lásd Inline assembly GCC-ben), az x0–x5 regisztereket vagy kimeneti/bemeneti megszorításként kell deklarálni, vagy szerepelniük kell a clobber-listában. Hiányzó bejegyzés esetén a fordító egy élő értéket tart nyilván ugyanabban a regiszterben, és a keletkező hibát semmi nem jelzi — a program „néha” rossz eredményt ad, optimalizálási szinttől függően.

5. Mentési aszimmetria. A fenti rutin levélfüggvény: nem hív mást, és csak az x0–x5 caller-saved (hívó által mentendő) regisztereket használja, veremhasználat nélkül. Amint a rutin bl-t kap vagy x19–x28-at használ, minden stp-hez tartoznia kell egy ldp-nek, és a veremmutatót 16 bájtos határra kell igazítani (AAPCS64 regiszterszerződés, Prológus és epilógus). Ellenpróba: a ret előtt mov sp, x29-szerű visszaállítás hiánya GDB-ben „nem oda visszatér” tünettel jelenik meg.

14.4.5 Fordítói védelmi kapcsolók (rövid említés)

A fenti ellenőrzéseket érdemes kiegészíteni, nem helyettesíteni a fordítói védelmi kapcsolókkal:

Fontos: ezek a következményt detektálják vagy nehezítik, nem az okot szüntetik meg. A canary a felülírás után szól, a határellenőrzés viszont az írás előtt. Ez a két mechanizmus nem versenytárs, hanem réteg. A konkrét kapcsolók elérhetősége és viselkedése az adott GCC-verzióhoz kötött, Ubuntu 24.04 LTS csomagolt toolchainjén ellenőrzendő (lásd az utolsó szakaszt).

14.4.6 Kódreview-checklista assembly-modult tartalmazó C projekthez

# Ellenőrzés Kérdés a review-n Bizonyíték
1 Szerződés rögzítve Hol van írva az argumentumok értékhatománya, az olvasható forráshossz, a megengedett átfedés, a közös állapot (reentrancia) és a hibajelzés? fejléc-komment, .h prototípus
2 Határellenőrzés a határon A hossz a fogadó kapacitásához van kötve? cmp/b.hi az első írás előtt
3 Ellenőrzés a konverzió után Az int→size_t konverzió előtt vagy után vizsgálunk? disassembly, objdump -d
4 Nulla mellékhatás elutasításkor Elutasítás után érintetlen-e az állapot? határérték-teszt kimenete
5 Mentési szimmetria Minden stp-nek van ldp párja minden kimeneti úton? objdump, minden ret áttekintése
6 Veremigazítás sp 16 bájtos többszöröse minden bl előtt? pro-/epilógus hossza
7 Callee-saved szabály Az x19–x28, x29, sp értéke helyreáll? AAPCS64-tábla összevetése
8 Clobber-lista / megszorítások Minden érintett regiszter deklarált inline asm-nál? GCC output, -S
9 Visszatérési érték konvenciója A -1 x0-ban, nem w0-ban képződik? disassembly
10 Fordítói védelmi kapcsolók Bekapcsoltak a fenti flagek az éles buildben? makefile, readelf -h (PIE), readelf -d (BIND_NOW)
11 Fuzz / határérték-tesztek Van teszt n = 0, n = max, n = max+1, NULL esetre? CI-kimenet

Ez a lista egyben a visszacsatolás és auditmódszer előképe: a Záróprojekt I auditlépése ugyanezeket a pontokat kéri számon, kiegészítve a mérési terv és a reprodukálhatóság ellenőrzésével.

14.4.7 Feladatok

1. feladat (magyarázó, alapszint — cél: megértés-ellenőrzés). Egy kollégád azt állítja: „elég, ha a határon ellenőrizzük, hogy a pointer nem NULL, mert a hosszt úgyis a hívó számolja”. Fogalmazz három ellenérvet, és add meg mindegyikhez azt a konkrét tesztesetet, amely megkülönbözteti a két álláspontot!

2. feladat (implementációs, középszint — cél: rutinépítés). Egészítsd ki a modult egy long lab_copy_n(const uint8_t *src, size_t n, size_t cap) rutinnal, amely a kapacitást paraméterként kapja, és ott is explicit határellenőrzést végez! Gondoskodj arról, hogy a C oldali int átadás esetén is helyes legyen a viselkedés, és hogy a rutin ne írjon a pufferen kívülre. Írj hozzá legalább négy határérték-tesztet!

3. feladat (hibakeresési/mérési, középszint — cél: rutinépítés). Adott egy variáns, amelyben a cmp x1, x2 utasítás a ciklus után szerepel, és a cbz a ciklus előtt. GDB-vel (a qemu-aarch64 -g és gdb-multiarch párossal) keresd meg, melyik konkrét tároló utasítás ír a puffer mögé az n = 64 esetben, majd egyszeri módosítással javítsd és igazold a javítást!

14.4.8 Megoldások

1. megoldás — Segítség. Gondold át külön: mit szűr ki a NULL-ellenőrzés, és mit NEM — egy pointer lehet nem-NULL és mégis érvénytelen, a hívó által adott hossz pedig maga is hibás lehet.

1. megoldás — Teljes megoldás. (a) A NULL-ellenőrzés csak egyetlen érvénytelen értéket szűr ki; egy 0x1000-re mutató vagy a szomszéd objektumba mutató pointer átmegy rajta, és az írás a puffer mögé kerül. Teszt: lab_copy_bounded((const uint8_t *)0x1000, 8) — a hívás nem NULL, mégis érvénytelen. (b) A hívó által számolt hossz maga is lehet hibás (túlcsorduló size_t szorzás, off-by-one), és a fogadó fél az egyetlen, aki ismeri a saját kapacitását. Teszt: n = LAB_CAP → várt -1. (c) A hívó és a hívott közötti szerződés nem kényszeríthető ki a hívó oldalon, mert a hívó lehet harmadik féltől származó kód. Teszt: az n=64 sor kimenete -1 kell legyen, függetlenül attól, ki hívja a rutint.

2. megoldás — Segítség. A sorrend számít: előbb a cap saját felső korlátját ellenőrizd (nehogy a hívó maga is túlígérjen), utána a kért n-t a cap-hez képest, és csak ezután induljon a másoló ciklus.

2. megoldás — Teljes megoldás. A kulcs a sorrend: először cap felső korlátja (a modul statikus puffere), majd a hívó által kért n összevetése a cap-pel, és csak ezután a ciklus. Vázlat:

lab_copy_n:                    /* x0=src, x1=n, x2=cap (A64 ISA) */
    cbz     x0, .Lrej
    mov     x6, #LAB_MAXN
    cmp     x2, x6             /* cap se lépje túl a statikus puffert */
    b.hi    .Lrej
    cmp     x1, x2             /* n <= cap ?  -- a konverzió UTÁN     */
    b.hi    .Lrej
    /* ... ugyanaz a másoló ciklus, n helyett x1 ... */

A tesztek: n = 0, cap = 63 → 0; n = 63, cap = 63 → 63; n = 64, cap = 63 → −1; n = 5, cap = 64 → −1 (a cap maga is korlátos); src = NULL → −1. Az int→size_t eset azért biztonságos, mert a negatív érték hatalmas előjel nélküli számmá alakul, és a b.hi úgyis elutasítja.

3. megoldás — Segítség. Számítsd ki pontosan, mely index a ciklus utolsó, még engedélyezett írása, és melyik index az, amelyre a lezáró tárolóutasítás ír — a kettő eltérése mutatja meg, hol csúszik túl a határon.

3. megoldás — Teljes megoldás. A hibás variánsban a ciklus legfeljebb a strb w5, [x3, x4] utasítással ír az x4 = 63 indexre (ez még a pufferen belül van), majd a lezáró strb wzr, [x3, x1] az x1 = 64 indexre ír — ez már a lab_buf utáni bájtra mutat. A GDB-ben break *lab_copy_bounded+... töréspontot érdemes a lezáró tárolóra tenni, majd x/8bx &lab_buf+60 paranccsal ellenőrizni a puffer környékét. A javítás: a cmp/b.hi párt a ciklus elé mozgatni, és a strb wzr indexét a már ellenőrzött n-re hagyni. Igazolás: az n=64 sor -1-et ad OK-val (a teszthajtó a puffer változatlanságát is ellenőrzi), az n=63 sor továbbra is 63, a kilépési állapot 0.

Mérési megjegyzés: a határellenőrzés költsége itt két utasítás (egy mov és egy cmp+b), ami elhanyagolható a másolási ciklus mellett; érdemi változás akkor várható, ha a rutin néhány bájtos bemenetre hívódik, és az ellenőrzés dominálja a futásidőt. QEMU-emulációban ez relatív/illusztratív adat, nem hardverhű abszolút ciklusszám.

14.4.9 Rövid összefoglaló

A C–assembly határ biztonsági szerződése négy dimenzióból áll: értékhatomány, pointer-érvényesség, tulajdonjog és hibajelzési konvenció. A linkelés egyiket sem ellenőrzi. Mivel a pointer érvényessége általánosan nem eldönthető, a legértékesebb ellenőrzés az, amely a kapott hosszt a fogadó fél saját kapacitásához köti, a konverzió után és az első írás előtt. A fordítói védelmi kapcsolók a következményt detektálják, nem az okot. A review-checklista tizenegy pontja (szerződés, határellenőrzés, mentési szimmetria, clobber-lista, hibajelzés, tesztek) közvetlenül a záróprojekt auditlépésére készít fel.

Továbbhasznált definíciók: - interop-szerződés: az argumentumok értékhatománya, a pointer-érvényesség értelmezése, a tulajdonjog és a hibajelzési konvenció együttese. - kapacitáskötött határellenőrzés: a fogadott hossz összevetése a hívott fél saját, fordítási időben ismert kapacitásával. - mentési szimmetria: minden regisztermentő utasításhoz tartozó, minden kimeneti úton végrehajtott helyreállítás.

14.4.10 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is fennmaradnak: a lab_a056.c + lab_bound.S párost a helyettesítő eszközlánccal (zig cc/Clang + musl, statikus build, Unicorn-alapú emulátor) lefordítottuk és lefuttattuk, a közölt kimenet és a 62/63/64-es táblázat mindhárom sora ebből származik; a GNU-eszközlánccal és qemu-aarch64-gyel nem futtattuk. A laborban ellenőrizendő: (1) az Ubuntu 24.04 LTS-ben csomagolt gcc-aarch64-linux-gnu, binutils-aarch64-linux-gnu és qemu-user konkrét verziója és a /usr/aarch64-linux-gnu sysroot-útvonal; (2) a -fstack-protector-strong tényleges viselkedése AArch64-en az adott GCC-verzióval; (3) a readelf -h által mutatott PIE-típus és a readelf -d BIND_NOW bejegyzése; (4) a qemu-aarch64 user-mode GDB-stubjának (-g) működése gdb-multiarch-gyal.