C/C++ programmerare, MPX coolaste HW-funktionen på länge

Permalänk
Datavetare

C/C++ programmerare, MPX coolaste HW-funktionen på länge

En typ av buggar som kan skapa rätt svåra buggar är off-by-one, användning av dangling pointers och liknande. Språk som Java, C#, Ruby, Python m.fl. lägger ju in tester mot att läsa/skriva utanför allokerat utrymme och de hanterar minne automatiskt.

Men finns fortfarande många problem där språk som C och C++ är och förblir toppvalet, fram till nu har man fått balansera på slak lina för att inte omedvetet orsaka problemen listade ovan.

Ni som har en Skylake eller senare har en funktion som kallas MPX, Memory Protection Extensions.

Kolla in detta program

/* -*- compile-command: "make CFLAGS=\"-mmpx -static-libmpx -fcheck-pointer-bounds -g -O0\" main" -*- */ #include <stdio.h> #include <stdint.h> typedef struct data_st { uint32_t pad1; uint32_t buff; uint32_t pad2; } DATA; void foo(uint8_t *b) { b[0] = 0xa; b[1] = 0xb; b[2] = 0xc; b[3] = 0xd; b[4] = 0xde; /* skriver en byte bakom det utrymme jag får använda! */ b[-1] = 0xad; /* skriver en byte framför det utrymme jag får använda! */ } void bar(uint32_t *d) { foo ((uint8_t *) d); } int main() { DATA data = { 0, 0, 0 }; bar (&data.buff); printf ("data.pad1 = %#x\n", data.pad1); printf ("data.buff = %#x\n", data.buff); printf ("data.pad2 = %#x\n", data.pad2); }

Kör man detta på normalt sätt får man utskriften

data.pad1 = 0xad000000 data.buff = 0xd0c0b0a data.pad2 = 0xde

Funktionen foo() skriver utanför både framför och bakom de 4 bytes som *b egentligen pekar på. I detta fall kraschar det inte då jag lagt en pad framför och bakom. Men det är en BUG!

Kolla nu vad som händer om man slår på MPX

Saw a #BR! status 1 at 0x4015e3 Saw a #BR! status 1 at 0x4015f7 data.pad1 = 0xad000000 data.buff = 0xd0c0b0a data.pad2 = 0xde

Trots att C-pekaren *b rent syntaktiskt totalt saknar information om hur stort område som pekas på upptäcks ändå både fallet där man skriver en byte före och en byte efter det område som initialt skickades till bar().

Kör jag detta i en debugger blir det ännu coolare, i det läget blir det som om man satt en brytpunkt precis innan man gör en minnesöverskrivning. "#BR" står just för att MPX upptäcker detta fel och genererar en "break-point trap".

Beskrev denna finnes på jobbet, har nog aldrig varit så lätt att få de som håller i pengarna att köpa in nya maskiner då jag pekade fördelen att ha detta på de maskiner som kör våra automatiskt unit-tester!

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Hedersmedlem

Häftigt. Det är inte ofta det kommer finesser som har potential att faktiskt påverka daglig verksamhet.

Permalänk
Medlem

Verkar häftigt, hur ser assemblern ut för programmet?
Hittade lite om hur det fungerar i GCC: https://gcc.gnu.org/wiki/Intel%20MPX%20support%20in%20the%20G...
så antar att bar(&data.pad1); genererar ett fel.

Visa signatur

flippy @ Quakenet

Permalänk
Datavetare
Skrivet av ante84:

Verkar häftigt, hur ser assemblern ut för programmet?
Hittade lite om hur det fungerar i GCC: https://gcc.gnu.org/wiki/Intel%20MPX%20support%20in%20the%20G...
så antar att bar(&data.pad1); genererar ett fel.

Är egentligen ingen magi alls, pseudoassembler ser ut ungefär så här

Vid anropet av bar() konstaterar kompilatorn att det man tar adressen av är fyra bytes långt. Vad som händer då är att MPX associerar en övre och en undre gräns på den buffert man når via pekaren (instruktion bndmk) som skickas som argument till bar(). Rent konkret lägger MPX till extra register (som även kan spilla över till RAM om allt för många buffertar är aktiva samtidigt), registren är bnd0 till bnd3.

I foo() har sedan kompilatorn lagt ut en MPX check på varje effektiv adress man kommer skriva till (bndcl och bndcu för att testa nedre och övre gräns). Är det som leder till den trevliga egenskapen att man hamnar på en brytpunkt INNAN skrivningen faktiskt sker när man kör i debugger.

Detta gör också det förväntande

void bar(uint32_t *d) { foo ((uint8_t *) d + 1); }

I detta läge är det i stället skrivningarna b[3] = 0xd; samt b[4] = 0xde; som skriver en respektive två bytes utanför. b[-1] = 0xad; är i detta fall korrekt och genererar inte heller någon varning från MPX.

D.v.s. det som i praktiken spåras är början och slutet på den buffert man når via en pekare, pekaren behöver inte peka just på första byte.

Den klurige inser direkt att MPX absolut inte kan plocka ALLA tänkbara fall. Ett specifikt fall som både är ett hål men också en finess ihop med MPX är C99 arrayer som deklareras med längd noll. Semantiskt betyder det ju detta

struct my_struct { int a; char b[0]; };

att något av typen struct my_struct innehåller medlemmen a följt av noll eller flera chars. Här är det ju omöjligt för kompilatorn att berätta för MPX var den bakre gränsen går. Man kan dock fortfarande upptäcka överskrivningar framför något som pekar på struct my_struct.

Jag ser främst MPX som ett hjälpmedel vid utveckling, framförallt då det mesta jag jobbar med körs på ARM och inte x86. Fungerar ju ändå lysande i t.ex. unit-tester, de testar ju endast att logiken är rätt, inte att det fungerar på ett specifikt system.

Är ändå fullt möjligt att använda MPX även i produktionskod. Det är en viss kostnad, men den är relativt liten och det är en kostnad alla som kör t.ex. Java/C# alltid betalar utan att det sätter allt för stora käppar i hjulen. I sann "x86-hacky" anda så är kodningen av MPX instruktioner gjorda så att modeller som inte stödjer MPX kommer tolka alla bndxx som varianter på nop (No OPeration). Så en binär med MPX-stöd går att köra på alla x86 CPUer!

Är ett hack, men TSX gör samma sak (modeller utan TSX uppför sig som att transaktionen alltid misslyckas så man måste gå "slow-path" som i detta fall är att göra på normalt sett med lås) och om det inte hade gått att köra binären på äldre modeller hade aldrig en sak som TSX stoppats in i Linux standardbibliotek för multitrådning (pthreads).

En defekt likt Heartbleed ska i praktiken vara omöjligt om man använder MPX, så finns garanterat områden där det är värt att ta en liten extrakostnad.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Datavetare
Skrivet av ante84:

Verkar häftigt, hur ser assemblern ut för programmet?
Hittade lite om hur det fungerar i GCC: https://gcc.gnu.org/wiki/Intel%20MPX%20support%20in%20the%20G...
så antar att bar(&data.pad1); genererar ett fel.

För att svara på det sista: ja, exakt samma rader kommer generera fel om man skickar in bar(&data.pad1);. Där är risken att det kraschar då man kommer skriva i minne jag som programkonstruktör inte riktigt vet vad det representerar för tillfället. Då detta minne ligger på "stacken" så finns risk att man skriver över returadress från funktionen, då lär det krascha!

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Yoshman:

För att svara på det sista: ja, exakt samma rader kommer generera fel om man skickar in bar(&data.pad1);. Där är risken att det kraschar då man kommer skriva i minne jag som programkonstruktör inte riktigt vet vad det representerar för tillfället. Då detta minne ligger på "stacken" så finns risk att man skriver över returadress från funktionen, då lär det krascha!

Menade att det blir ett mindre fel eftersom bara b[-1] i foo genererar fel precis som i narrowing exemplen i länken. Men det verkar som det går att påverkar hur det fungerar med kompilator växlar.

Edit: Går att fixa med växeln -fchkp-first-field-has-own-bounds så den inte tar storleken för hela structen.

Visa signatur

flippy @ Quakenet