Ochrona pamięci w przypadku czcionek internetowych

Dominik Röttsches
Dominik Röttsches
Rod Sheeter
Rod Sheeter
Chad Brokaw
Chad Brokaw

Data publikacji: 19 marca 2025 r.

Skrifa jest napisana w języku Rust i została stworzona jako zamiennik FreeType, aby przetwarzanie czcionek w Chrome było bezpieczne dla wszystkich użytkowników. Skrifa korzysta z bezpieczeństwa pamięci w języku Rust i pozwala nam szybciej wprowadzać ulepszenia technologii czcionek w Chrome. Przejście z FreeType na Skrifa pozwala nam na elastyczność i pewność podczas wprowadzania zmian w kodzie czcionek. Teraz poświęcamy znacznie mniej czasu na naprawianie błędów związanych z bezpieczeństwem, co przekłada się na szybsze aktualizacje i lepszą jakość kodu.

W tym poście wyjaśniamy, dlaczego Chrome przestał używać FreeType, i przedstawiamy ciekawe szczegóły techniczne dotyczące ulepszeń, które umożliwiło to przejście.

Dlaczego warto zastąpić FreeType?

Internet jest wyjątkowy, ponieważ umożliwia użytkownikom pobieranie niezaufanych zasobów z wielu niezaufanych źródeł z oczekiwaniem, że wszystko będzie działać i że jest to bezpieczne. To założenie jest na ogół prawidłowe, ale dotrzymanie tej obietnicy użytkownikom wiąże się z kosztami. Aby na przykład bezpiecznie używać czcionki internetowej (czcionki dostarczanej przez sieć), Chrome stosuje kilka środków bezpieczeństwa:

Chrome jest dostarczany z FreeType i używa go jako głównej biblioteki do przetwarzania czcionek w systemach Android, ChromeOS i Linux. Oznacza to, że w przypadku wykrycia luki w FreeType wielu użytkowników jest narażonych na niebezpieczeństwo.

Biblioteka FreeType jest używana przez Chrome do obliczania metryk i wczytywania obrysów z czcionek. Ogólnie rzecz biorąc, używanie FreeType było dla Google ogromnym sukcesem. Wykonuje złożone zadania i robi to dobrze. W dużym stopniu na niej polegamy i wnosimy do niej swój wkład. Jest jednak napisana w niebezpiecznym kodzie i pochodzi z czasów, gdy złośliwe dane wejściowe były mniej prawdopodobne. Samo śledzenie strumienia problemów wykrytych przez fuzzing kosztuje Google co najmniej 0,25 etatu inżyniera oprogramowania. Co gorsza, nie znajdujemy wszystkiego lub znajdujemy problemy dopiero po udostępnieniu kodu użytkownikom.

Ten wzorzec problemów nie jest unikalny dla FreeType. Zauważamy, że inne niebezpieczne biblioteki mają problemy nawet wtedy, gdy korzystamy z najlepszych inżynierów oprogramowania, przeprowadzamy inspekcję kodu przy każdej zmianie i wymagamy testów.

Dlaczego problemy się pojawiają

Podczas oceny bezpieczeństwa FreeType zaobserwowaliśmy 3 główne klasy problemów (niepełne):

Używanie niebezpiecznego języka

Wzorzec/problem Przykład
Ręczne zarządzanie pamięcią
Niesprawdzone uzyskiwanie dostępu do tablicy CVE-2022-27404
Przepełnienia liczb całkowitych Podczas wykonywania wbudowanych maszyn wirtualnych na potrzeby wskazówek TrueType dotyczących rysowania i wskazówek CFF
https://issues.oss-fuzz.com/issues?q=FreeType%20Integer-overflow
Nieprawidłowe użycie alokacji z zerowaniem lub bez zerowania Dyskusja w https://gitlab.freedesktop.org/freetype/freetype/-/merge_requests/94, 8 problemów z fuzzerem znalezionych później
Nieprawidłowe rzutowanie Zobacz następny wiersz dotyczący użycia makra

Problemy specyficzne dla projektu

Wzorzec/problem Przykład
Makra ukrywają brak jawnego określenia rozmiaru
  • Makra takie jak FT_READ_* i FT_PEEK_* ukrywają, jakich typów liczb całkowitych się używa, co uniemożliwia używanie typów C99 z jawnymi rozmiarami (int16_t itp.)
Nowy kod konsekwentnie dodaje błędy, nawet jeśli jest napisany w sposób defensywny.
  • Obsługa COLRv1 i OT-SVG powodowała problemy
  • Fuzzing znajduje niektóre, ale niekoniecznie wszystkie, #32421, #52404
Brak testów
  • Tworzenie czcionek testowych jest czasochłonne i trudne

Problemy z zależnościami

Fuzzing wielokrotnie wykrywał problemy w bibliotekach, od których zależy FreeType, takich jak bzip2, libpng i zlib. Porównaj na przykład freetype_bdf_fuzzer: Use-of-uninitialized-value w inflate.

Fuzzing nie wystarczy

Fuzzing – automatyczne testowanie z użyciem szerokiego zakresu danych wejściowych, w tym losowych nieprawidłowych – ma na celu wykrycie wielu typów problemów, które trafiają do stabilnej wersji Chrome. Fuzzing FreeType jest częścią projektu oss-fuzz Google. Wykrywa problemy, ale czcionki okazały się dość odporne na fuzzing z tych powodów:

Pliki czcionek są złożone, porównywalne z plikami wideo, ponieważ zawierają wiele różnych typów informacji. Pliki czcionek to format kontenera dla wielu tabel, z których każda służy do innego celu w przetwarzaniu tekstu i czcionek w celu uzyskania prawidłowo umieszczonego glifu na ekranie. W pliku czcionki znajdziesz:

  • Statyczne metadane, takie jak nazwy czcionek i parametry czcionek zmiennych.
  • Mapowania znaków Unicode na glify.
  • Złożony zestaw reguł i gramatyka układu glifów na ekranie.
  • Informacje wizualne: kształty glifów i informacje o obrazach opisujące wygląd glifów umieszczonych na ekranie.
    • Tabele wizualne mogą z kolei zawierać programy wskazówek TrueType, które są miniprogramami wykonywanymi w celu zmiany kształtu glifu.
    • Ciągi znaków w tabelach CFF lub CFF2, które są imperatywnymi instrukcjami rysowania krzywych i wskazówek wykonywanymi w silniku renderowania CFF.

Złożoność plików czcionek jest równoważna z własnym językiem programowania i przetwarzaniem maszyny stanowej, co wymaga do ich wykonania specjalnych maszyn wirtualnych.

Ze względu na złożoność formatu fuzzing ma ograniczenia w znajdowaniu problemów w plikach czcionek.

Trudno jest osiągnąć dobre pokrycie kodu lub postęp fuzzer z tych powodów:

  • Fuzzing programów wskazówek TrueType, ciągów znaków CFF i układu OpenType za pomocą prostych mutatorów typu bit-flipping/shift/insertion/deletion ma trudności z osiągnięciem wszystkich kombinacji stanów.
  • Fuzzing musi przynajmniej tworzyć częściowo prawidłowe struktury. Losowa mutacja rzadko to robi, co utrudnia osiągnięcie dobrego zasięgu, zwłaszcza w przypadku głębszych poziomów kodu.
  • Obecne działania związane z fuzzingiem w ClusterFuzz i oss-fuzz nie używają jeszcze mutacji uwzględniającej strukturę. Użycie mutatorów uwzględniających gramatykę lub strukturę może pomóc uniknąć tworzenia wariantów, które są odrzucane na wczesnym etapie, ale kosztem dłuższego czasu opracowywania i wprowadzenia szans, które pomijają części przestrzeni wyszukiwania.

Aby fuzzing mógł się rozwijać, dane w wielu tabelach muszą być zsynchronizowane:

  • Zwykłe wzorce mutacji fuzzerów nie tworzą częściowo prawidłowych danych, więc wiele iteracji jest odrzucanych, a postęp jest powolny.
  • Mapowanie glifów, tabele układu OpenType i rysowanie glifów są połączone i od siebie zależne, tworząc przestrzeń kombinatoryczną, której rogi trudno osiągnąć za pomocą fuzzingu.
  • Na przykład znalezienie luki o wysokim stopniu ważności tt_face_get_paint COLRv1 zajęło ponad 10 miesięcy.

Pomimo naszych starań problemy z bezpieczeństwem czcionek wielokrotnie docierały do użytkowników. Zastąpienie FreeType alternatywą w języku Rust zapobiegnie wielu całym klasom luk w zabezpieczeniach.

Skrifa w Chrome

Skia to biblioteka graficzna używana przez Chrome. Skia polega na FreeType, aby wczytywać metadane i kształty liter z czcionek. Skrifa to biblioteka Rust , która jest częścią rodziny bibliotek Fontations i stanowi bezpieczny zamiennik części FreeType używanych przez Skia.

Aby przejść z FreeType na Skia, zespół Chrome opracował nowy backend czcionek Skia oparty na Skrifa i stopniowo wprowadzał zmiany dla użytkowników:

W przypadku integracji z Chrome polegamy na płynnej integracji języka Rust z bazą kodu wprowadzoną przez zespół ds. bezpieczeństwa Chrome.

W przyszłości przejdziemy na Fontations również w przypadku czcionek systemu operacyjnego, zaczynając od systemów Linux i ChromeOS, a następnie Androida.

Bezpieczeństwo przede wszystkim

Naszym głównym celem jest zmniejszenie (lub najlepiej wyeliminowanie) luk w zabezpieczeniach spowodowanych dostępem do pamięci poza zakresem. Rust zapewnia to od razu, o ile unikasz bloków kodu unsafe.

Nasze cele dotyczące wydajności wymagają od nas wykonania jednej operacji, która jest obecnie niebezpieczna: reinterpretacji dowolnych bajtów jako struktury danych o silnym typie. Pozwala nam to odczytywać dane z pliku czcionki bez wykonywania niepotrzebnych kopii i jest niezbędne do utworzenia szybkiego parsera czcionek.

Aby uniknąć własnego niebezpiecznego kodu, postanowiliśmy przekazać tę odpowiedzialność bibliotece bytemuck, która jest biblioteką Rust zaprojektowaną specjalnie do tego celu i jest szeroko testowana i używana w całym ekosystemie. Koncentrowanie reinterpretacji surowych danych w bytemuck zapewnia, że ta funkcja jest w jednym miejscu i jest audytowana, a także pozwala uniknąć powtarzania niebezpiecznego kodu na potrzeby tego celu. Projekt safe transmute ma na celu włączenie tej funkcji bezpośrednio do kompilatora Rust. Przełączymy się na nią, gdy tylko będzie dostępna.

Poprawność ma znaczenie

Skrifa jest zbudowana z niezależnych komponentów, w których większość struktur danych jest zaprojektowana jako niezmienna. Zwiększa to czytelność, łatwość konserwacji i wielowątkowość. Ułatwia też testowanie jednostkowe kodu. Wykorzystaliśmy tę okazję i stworzyliśmy zestaw około 700 testów jednostkowych, które obejmują cały stos, od procedur analizowania niskiego poziomu po maszyny wirtualne wskazówek wysokiego poziomu.

Poprawność oznacza też wierność, a FreeType jest wysoko ceniony za generowanie wysokiej jakości obrysów. Aby być odpowiednim zamiennikiem, musimy osiągnąć tę samą jakość. W tym celu stworzyliśmy specjalne narzędzie o nazwie fauntlet, które porównuje dane wyjściowe Skrifa i FreeType w przypadku partii plików czcionek w szerokim zakresie konfiguracji. Daje nam to pewność, że możemy uniknąć regresji jakości.

Ponadto przed integracją z Chromium przeprowadziliśmy w Skia szeroki zestaw porównań pikseli , porównując renderowanie FreeType z renderowaniem Skrifa i Skia, aby mieć pewność, że różnice pikseli są minimalne we wszystkich wymaganych trybach renderowania (w różnych trybach antyaliasingu i wskazówek).

Testowanie za pomocą fuzzingu jest ważnym narzędziem do określania, jak oprogramowanie będzie reagować na nieprawidłowe i złośliwe dane wejściowe. Od czerwca 2024 r. nieustannie testujemy nasz nowy kod za pomocą fuzzingu. Obejmuje to same biblioteki Rust i kod integracji. Fuzzer znalazł (w momencie pisania tego tekstu) 39 błędów, ale warto zauważyć, że żaden z nich nie był krytyczny z punktu widzenia bezpieczeństwa. Mogą one powodować niepożądane efekty wizualne lub nawet kontrolowane awarie, ale nie prowadzą do luk w zabezpieczeniach, które można wykorzystać.

Dalej!

Jesteśmy bardzo zadowoleni z wyników naszych działań związanych z używaniem języka Rust do obsługi tekstu. Dostarczanie użytkownikom bezpieczniejszego kodu i zwiększanie produktywności deweloperów to dla nas ogromny sukces. Planujemy nadal szukać możliwości używania języka Rust w naszych stosach tekstowych. Jeśli chcesz dowiedzieć się więcej, Oxidize przedstawia niektóre przyszłe plany Google Fonts.