Clang memory sanitizer hiba, vagy saját hiba?
2021-06-17T23:05:38+02:00
2021-06-21T15:51:23+02:00
2022-08-12T03:20:32+02:00
senki
Egy random - eredetileg amúgy winen fejlesztett projektet áttettem linux alá, hogy kicsit megvalgrindoljam, meg ilyesmi. Szépen kiszedtem a minimális leak-eket belőle, hibákra amúgy meg eddig se futott atekintetben amit mutatni fogok, de a clang memory sanitizer jelölt egy uninitialized memory access-t, ami rossz ómen, szóval utána akartam járni.

A furcsa dolog, hogy a unit tesztben el is érem az adott területet és pont a megfelelő tartalom van benne - sőt a sanitizer-el build-elt binárist debugolva is ki tudom olvasni ott a helyes értékeket - lásd kép.

A képen kívül felvettem egy ilyen asciinema castot is kicsit több részlettel - de figyelve, hogy felesleges részek ne legyenek mutogatva a kódbázisból azért, csak ez az érdekesség. A kódot már széttúrtam a debuggoláshoz / analízishez, azért vannak ilyen nevek és ifek benne - mert már azt is meg akartam tudni, hogy azzal van-e gondja, hogy referenciát adok vissza a statikusan feltöltött vektorban lévő stringekre, vagy pointerrel jobb-e, más pontján a kódnak jobb-e stb.

Ezt itt tudjátok megnézni - ha bárkit érdekel:
Is this clang address-sanitizer bug?

Egy -D_GLIBCXX_DEBUG mellett buildelve egyébként nem jön elő - és a gcc address sanitizer-el sem jön semmi. Szerintem a sanitizernek talán kell, hogy a clang standard library-hoz legyen linkelve a progi szóval gondolom ez csak annyit jelent, hogy akkor nem is fut le ez a teszt.

Nézegettem az asm-et is kicsit egyesével léptetve, de csak annyit tudtam meg, hogy van valami számláló (vagy flag) amit ellenőríz, hogy nulla-e és annak fényében írja a sanitizer a hibaüzenetet. Szóval lehet simán, hogy ő tartja karban rosszul, de nem láttam azért még a sanitizert hibázni azt hiszem.

Egyébként mint mondtam, bugot nem okoz, de engem zavar. Az ascii-cast előtt egyébként full clean build-et nyomtam egyébként.

Nem tudom találkozott-e valaki ilyennel.

ui.: Ezek a HACKERMAN ifek tényleg csak azért vannak, hogy több helyen ki tudjam printf-elni a dolgot még mielőtt a függvény visszatér, de azért a korábbi cuccokat ne befolyásolja. Az összes ilyet kirevertáltam a kódból, szóval nem látni azért ilyeneket normális esetben
Mutasd a teljes hozzászólást!
Csatolt állomány
Lefordítottál mindent is? A libc++ a gtest-et és minden opensource kódot és társait, szóval mindent. Ha nem, akkor ez a gond. Mindent instrumentálni kell.

"It is critical that you should build all the code in your program (including libraries it uses, in particular, C++ standard library) with MSan. See MemorySanitizerLibcxxHowTo for more details."

up: szóval úgy, ahogy előttem is írták (csak késő olvastam el)
Mutasd a teljes hozzászólást!

  • Simán lehet false positive.

    Ilyen kitétel is van:

    MemorySanitizer requires that all program code is instrumented. This also includes any libraries that the program depends on, even libc. Failing to achieve this may result in false reports. For the same reason you may need to replace all inline assembly code that writes to memory with a pure C/C++ code.

    MemorySanitizer — Clang 13 documentation
    Mutasd a teljes hozzászólást!
  • Megnéztem a videót, elég sok a pointerezés..
    Amire érdemes lehet figyelni és hibát okozhat, hogy a tesztek között a non local static dolgok nem inicializálódnak újra, azok csak egyszer inicializálódnak a main előtt. Láttam, hogy használsz ilyesmit (VSForms::Kinds()). Lehet, hogy a memóriában még/már ott vannak az elvárt értékek, de ez csak szerencse.
    Mutasd a teljes hozzászólást!
  • Megnéztem a videót, elég sok a pointerezés..

    Egyébként azok eredetileg nem voltak pointerek, hanem referenciákat adtam a static vectorre és annak a main előtt inicializált stringjeire a megfelelő függvényeknél. A pointerekre azért írtam ezt át, mert gondoltam hátha a reference overuse-tól őrül meg, de nem.

    Amire érdemes lehet figyelni és hibát okozhat, hogy a tesztek között a non local static dolgok nem inicializálódnak újra, azok csak egyszer inicializálódnak a main előtt. Láttam, hogy használsz ilyesmit (VSForms::Kinds()). Lehet, hogy a memóriában még/már ott vannak az elvárt értékek, de ez csak szerencse.

    Ez viszont érdekesnek hangzik. Kifejtenéd ezt kicsit jobban / belemegyünk egy kérdezz-felelekbe ebben?
    Azért mert ezen a részen igazából kvázi egy saját típusrendszer van kialakítva trükkökkel, makrókkal stb.

    A vége egyébként egy ilyesmi, ilyeneket tudsz írni utána: VertexStructure<Pos_3f, Nor_3ub>(..), meg VertexStructure<Uv_2f, Pos_3f, Nor_3f>(..) és ezekhez "nagyrészt compile time" és kis részt így a main előtt, statikus inicializáláskor a típusokhoz kiszámítódik minden szükséges meta-adat. Tehát ezekből lehet mondjuk egy vektorod, de az elemei normálisan, tömötten egymás után helyezkednek el és kigenerálódik hozzájuk "kényelmes" accessor, mint vstruct.x, vagy vstruct.i / vsform.uv[0].x stb. (ez utóbbiak tényleg ugyanarra fordulnak, mintha bele-indexelnél egy raw byte tömbbe simán mondjuk float-ra cast-olva) az API-k meg ilyen buffereket használnak a meta-adataikkal. A VSForm itt például Pos_3f, meg Nor_3ub, stb.

    Ezeket magyarázatként mondom. A Pos_3f egyébként egy class és öröklődik a VSForm-ból. A VSForm-ban van egy uint32_t ami egy "handle" és vannak statikusan ilyen tömbök, mint ez a Kinds() tömb is (de több ilyen van - ezek gyakorlatilag "mezők" és "függvények". Úgy van kialakítva a dolog, hogy a VSForm protected konstruktora várja ezt a handle-t a leszármazott pedig egy statikus inline mezőbe kiszámolja a saját handle-értékét (ami közös minden példány között, mert ugye ugyanolyan típusúak) és eközben beleteszi saját magának megfelelő értékeit a Kinds()-hoz hasonló statikus tárakba miközben kap egy handle-t, hogy hanyadik épp ő ezekben a tömbökben. Lényegében a handle egy index, amivel ezen statikus tömböket lehet indexálni. A statikus "HandleVal" field-et, amibe a típus handle-je számítódik ki továbbá minden példány konstruktor felhasználja a VSForm apa-konstruktor hívásához, így a fordító semmiképpen se optimalizálja ki.


    No mármost... A VSForm-ban amivel el lehet érni ezeket a statikus adatokat az protected, csak a leszármazottak felé van láthatósága és a main előtt, a Pos_3f::HandleVal meg hasonló értékek előre kiszámítása közben írkálnak bele ezek a leszármazott formák. Más nem piszkálja és normális esetben nem is tudja piszkálni ezt a non-local static dolgot.

    Tehát pont az az elvárás, hogy 

    a non local static dolgok nem inicializálódnak újra, azok csak egyszer inicializálódnak a main előtt

    Viszont ez kicsit aggaszt most:

    Lehet, hogy a memóriában még/már ott vannak az elvárt értékek, de ez csak szerencse.

    Nem tudok olyanról, hogy ezeknek memória hulladéknak kéne lennie random helyeken miután egyszer inicializálódtak már a main előtt és remélem nem ilyenre gondolsz itt. Nem thread-local static-ok, így arról se igazán lehet szó, hogy más szálban lenne a teszt vagy ilyesmi és könnyen le tudom csekkolni, hogy senki sem éri el ezeket a non-local static vektorokat. Továbbá a "Kinds()" és hasonló függvények, amik a statikus vektorokat tárolják rendesen a .cpp fájlban külön vannak, nem inline-ként a headerben. Ez utóbbi lenne talán csak baj szerintem, ha inline függvény lenne a headerben, mert tudnák azon aggódni (bár szerintem nem szabadna), hogy más pontján a proginak más-más példány lehetne a függvényből és mind egy-egy lokális static vektorral (vagyis sok lenne párhuzamosan belőlük - de szerintem ez akkor se lenne probléma, csak ki kéne olvassam a szabványt akkor). Úgy, hogy ez szépen ki van húzva a .cpp fájlba szerintem a függvényben definiált static-ból azért mindenképpen közös példány van globálisan - mármint nincs arról szó, hogy ez a kód egy DLL része is, meg egy progi része is mondjuk mert akkor nyilván külön lenne, de szerintem érthető mire gondolok.

    Ilyen feltételek mellett, hogy akár breakpoint-al is lekövetve tudom, hogy az elején a GetKinds()-ot csak a VSForm leszármazottai használják, később meg csak read-only használom is szerencse, hogy csak az elvárt értékek vannak benne? Ha igen, szerintem fontos lenne megértenem, hogy miért.

    + Ha valami kérdés van szívesen válaszolok, ameddig még úgy érzem nem írom ki a full source kódot azért .

    MemorySanitizer requires that all program code is instrumented. This also includes any libraries that the program depends on, even libc. Failing to achieve this may result in false reports.

    Eredetileg a saját libc-jével, standard módon fordítottam a clang++ fordítóval. A -D_GLIBCXX_DEBUG igazából csak azért lett, mert arch-on (amit nagyon szeretek, de ezt konkrétan pont nem szeretem benne) nincsenek általában "-dbg" szimbólumos csomagok, szóval ha debug szimbólumokat akarok olyan egyszerű dolgokra, mint a string-ek, akkor fordíthatnék egy debug-symbol-os saját clang standard libet :D. De aztán úgy voltam vele, hogy akkor ki-printf-álom ami érdekel, vagy átteszem egy const char*-ba ideiglenesen meg ilyenek, most az ingerküszöböt még nem érte el a dolog, csak tudtam, hogy a glibc-hez már vannak szimbólumaim.

    De normális esetben ezt a kapcsolót most nem használtam - a "videó"-ban se már egyébként. Amikor használtam akkor egy input-string-steam-re adott valami bajt, ami szerintem tényleg fals pozitív volt, meg ugye azt is megmagyarázza ez, hogy miért hiányzott a fél instrumentáció ezek szerint. Ez mindenképp hasznos infó!
    Mutasd a teljes hozzászólást!
  • Egyébként normális esetben ezek a kódok - amikor nincsenek széjjelcseszve debuggolási célból:

    // VSForm.cpp: std::vector<std::string> &VSForm::Kinds() { // Only initialized on first call static std::vector<std::string> static_kinds; // Returning reference or ptr to it fixes ordering issues as this is function return static_kinds; } // VSForm.h: /** * desc -> kind * * BEWARE: Do not use from destructors that run when system is shutting down! * The following became function because of "static initialization order problem" evasive strategy v2 - see VSForm.cpp!!! */ static std::vector<std::string> &Kinds(); /** * Useful for runtime knowing what the vertex structure form represents. Can return "POSITION", "NORMAL", "UV", etc. * * BEWARE: Do not use this in ANY child-class destructor unless you want to shoot yourself in the foot! */ inline const static std::string &GetKindOf(VSForm v) { return VSForm::Kinds()[v.desc]; }
    A tesztben meg eredetileg ez szerepelt csak - minden pointerezés nélkül:

    ... // Runtime-kind-informatiion tests ASSERT_STR_EQ("POSITION", (VSForm::GetKindOf(format1.getForms()[0])).c_str()); ASSERT_STR_EQ(Pos_3f::Kind, (VSForm::GetKindOf(format1.getForms()[0])).c_str()); ...
    Szóval eredetileg a memory sanitizer error az strcmp instrumentálásából jött ki, csak referenciák voltak és pointerek egyáltalán sehol se nem voltak. Azokat akkor írtam bele, amikor szerettem volna debuggolás közben próbálgatni a dolgokat, vagy jobban megérteni mire panaszkodik. A plusz printf-ek, meg az a hackerman beállítható debugváltozó is csak ezután mentek be, hogy még a GetKindOf() legkülső szintje előtt vajon már ott panaszkodik-e a sanitizer meg ilyenek, vagy ki tudom legalább printf-álni a dolgokat.

    Ez nyilván "valamivel" tisztább és érthetőbb kód volt, mint a videóban lévő, de debugolás szempontjából talán az ad több infót - meg amikor csináltam éppen úgy szét volt szedve a kód már.
    Mutasd a teljes hozzászólást!
  • Lefordítottál mindent is? A libc++ a gtest-et és minden opensource kódot és társait, szóval mindent. Ha nem, akkor ez a gond. Mindent instrumentálni kell.

    "It is critical that you should build all the code in your program (including libraries it uses, in particular, C++ standard library) with MSan. See MemorySanitizerLibcxxHowTo for more details."

    up: szóval úgy, ahogy előttem is írták (csak késő olvastam el)
    Mutasd a teljes hozzászólást!
  • No ez viszont számomra újdonság... A libc++ból gondoltam, hogy clang-osat kell használnom, de fejben nem állt össze, hogy azt mindenképp le kell fordítanom source-ból lényegében ehhez a sanitizerhez - ez nem tudom mindig így működött-e, vagy csak ahol utoljára használtam, ott már a build része volt ez és nem figyeltem fel erre, de akkor rá kellett volna jöjjek, amikor a glibcxx-el keverve kiderült, hogy ott azért megy máshogy / másmerre mert van ez az instrumentáció...

    Na majd kipróbálom mi a helyzet, ha lefordítok neki instrumentáltan egy libc++t, mert ez megmagyaráz most mindent.
    Ha bejön (valszeg ez a baja), akkor nem tudom kinek adjak pontot, vagy lehet-e itt felezni, mert mindkettőtök kellett azért ehhez, hogy rájöjjek mi is itt a gixer - ami ezek szerint igazából sima RTFM haha - de valamikor sok időkkel ezelőtt használtam ezt valahol és ezért utána se néztem. Vagy régen nem is volt még instrumentálva (ez elég rég volt), vagy ahogy mondtam egy meglévő projekt kész build-jébe már benne volt és könnyű volt nem felfigyelni rá, hogy egy saját fordítású libc++ ott van neki...
    Mutasd a teljes hozzászólást!
  • Jah, ez volt a gixer. Benézés jellegű akkor ez, de vagy kevertem az address sanitizer-el, vagy korábban ahol láttam, ott benne volt a build-be, hogy csinálja meg, vagy korábban instrumentált stdlib nélkül is ment ez.

    Lehet valahogy felezni pontot? Ha nem valószínűleg azért adom ktamail-nek, mert a linkje segített a megoldáshoz, de már a korábbi **G** hozzászólásoknál látnom kellett volna talán ha jobban figyelek.

    ui.: Egyébként nem volt hiba. Betettem egy hibát tesztelésképpen, azt nem szűrte épp ki (de a valgrind igen), aztán kicsit piszkáltam, hogy egy if is hivatkozzon rá és ki is szűrte, de az egész kódbázison átmegy most minden jelzés nélkül ez is, meg a valgrind is.
    Mutasd a teljes hozzászólást!
abcd