Blog.nechutny.net

Blog o webu a IT.

wtf.php

V současné době intenzivně pracuji na své bakalářské práci, která se zabývá tvorbou nástroje pro automatizovaný převod zdrojových kódu v podmnožině jazyka PHP do C++. Takový kód pak je možné zkompilovat a zavést jako rozšíření PHP. Je tedy možné takto transformovat různé knihovny do stavu, kdy jsou až násobně rychlejší a optimálnější. Některé testy momentálně ukazují až 200 násobné zrychlení.

O čem je však tento článek? Při práci na tomto nástroji se zabývám podrobně jazykem PHP, prolézám jeho zdrojové kódy, dokumentaci a testuji různé okrajové záležitosti. Rozhodl jsem se sepsat některé WTF věcí o kterých třeba ani nevíte i když PHP používáte dlouho.

Porovnání

Začněme něčím relativně známým. To, že PHP obsahuje dva operátory pro porovnání, je poměrně známé a odlišnosti zná také téměř každý - == porovná pouze hodnotu a tedy var_dump(5 == "5"); bude pravdivé. U porovnání prostřednictvím === bude pak výsledek obdobného kódu var_dump("5" === 5); již nepravdivý.

Předchozí sloupec popsal rozšířené znalosti o rozdílu těchto operátorů. Zajímavější ovšem je, co plyne z onoho automatického přetypování. Co myslíte, bude pravda var_dump('1000' == '1e3');? Ano bude jak v PHP 5.6, tak PHP 7. I přesto, že obě proměnné jsou řetězce a mají evidentně rozdílné hodnoty.

Co to znamená? Pokud používáte PHP starší, než verze 5.4.4, tak můžete mít chybu třeba v přihlašování uživatelů. Představme si situaci, že někdo používá md5 pro ukládání hesel (opravdu jen představme a doufejme, že již nikdo nepoužívá něco tak nebezpečného). Ono to však bude fungovat i u hashovacích funkcí z rodiny SHA a dalších, jejichž výstup je ve formátu hexadeciminálního zápisu. V databázi pak bude uživatel třeba s heslem, jehož hash je 12345678901234567890123456789012. V situaci, kdy se použije porovnání prostřednictvím operátoru == dojde k převodu hashe z DB i vypočteného funkcí md5 do typu float a k chybě zaokrouhlení.

var_dump('12345678901234567890123456789012' == '12345678901234567890123456789013');
je TRUE i když se poslední číslice neshoduje. Od 16. znaku již dojde k zaokrouhlení a došlo tedy k oslabení. Místo původně nutných kombinací k vyzkoušení postačí jen 10^16. To je výrazně nižší číslo oproti 16^32 - tedy za předpokladu, několika faktorů:
  1. Víme, že hash začíná pouze numerickými znaky (třeba únik zálohy DB). Pokud nevíme, tak máme 0.0541% pravděpodobnost, že tomu tak bude.
  2. Víme jaký řetězec generuje jaký hash (Můžeme použít předpočtené tabulky).
Co si z toho odnést? Výrazně omezit porovnání prostřednictvím operátoru ==.

Priorita operátorů

Další zajímavostí, která je sice dobře zdokumentovaná, ale ve většině knížek vás na to neupozorní, je priorita operáorů. Podívejte se na prioritu logických operátorů zapsaný jako &&, či || a pak na prioritu klíčových slov and a or. Vidíte to? Jasně, mezi těmito operátory je v precedenční tabulce další řádek obsahující poměrně podstatné operátory jako =, += apod.

Co to znamená? Nejjednodušší je to ukázat na kódu.

$a = TRUE AND FALSE;
var_dump($a);

$a = TRUE && FALSE;
var_dump($a);
Zkusili jste si? Proč to tak je? Operátor přiřazení má vyšší prioritu, než operátor AND. Dojde tedy napřed k přiřazení TRUE do proměnné $a a poté až k operaci AND, jejíž výsledek se zahodí. Lépe je to vidět na ekvivalentním kódu ($a = TRUE) AND FALSE;. V druhém případě s operátorem && dojde k očekávanému chování odpovídající kódu $a = (TRUE && FALSE); a výsledek v proměnné $a bude tedy FALSE.

Problém je celkem zřejmý. Kvůli absenci výjimek u vestavěných funkcí v PHP je celkem častý zápis na styl $success = foo() AND bar(); Z proměnné $success se však v tomto případě nedozvím, zda uspěla i druhá funkce.

Velikost datových typů

Jaké největší číslo je možno v PHP uložit do proměnné typu int? To záleží na mnoha faktorech. PHP má v paměti všechna čísla jako signed (se znaménkem). Tedy jeden bit je vyhrazen pro znaménko. Následně pak záleží, zda máte 32 bitové, nebo 64 bitové PHP. Tak je to na Linuxu, ale na Windows je to o něco jednodušší. Ať už máte 32 bitové, nebo 64 bitové PHP, tak čísla budou vždy 32 bitová. No, není to zrovna výhra.

S čím je pak problém? Když použijete třeba funkci crc32 a chcete hodnotu porovnat s něčím z DB - jedno číslo máte jako string a druhé jako int. Pořád nechápete? crc32 vrací 32 bitů, které jsou reprezentovány jako číslo. Tedy nejvyšší, nebo nejnižší bit (v závislosti na little/big endian) bude znaménko a pokud bude zrovna nastaven na jedničku, tak nedostanete stejné číslo, jako máte ve stringu. Porovnání pak bude v 50% správných případů nepravdivé. Řešením je pak trošku nehezký převod prostřednictvím konstrukce printf("%u", crc32($foo));, která umožní vzít číslo jako unsigned a převést ho na string. Mysleli jste, že v PHP nebudete muset řešit takovéhle low level věci? Chyba.

T_PAAMAYIM_NEKUDOTAYIM

Už jste se setkali s tímhle tokenem? Nevíte? Ten název byste si určitě (ne)pamatovali. Tahle šílenost je hebrejský výraz pro dvě dvojtečky (::). Ty, které použijete u static tříd, či konstant. Tohle tu je už od PHP 3, kdy jistý izraleský vývojář použil tento název a přeci to nebudem měnit, když už si na to všichni zvykli... No, zůstalo to i do dnešního PHP 7, kdy někdy je to již T_DOUBLE_COLON, ale stále v některých případech dostanete i tuhle šílenost.

Zajímavý je článek od Phil Sturgeon - T_PAAMAYIM_NEKUDOTAYIM v Sanity, kde rozebírá blíže historii tohoto tokenu.