Pretensje do kodu

Za pomocą Insure++ można łatwo włączać/wyłączać śledzenie bez konieczności pełnej rekompilacji. W tym celu wystarczy jedynie dolinkować odpowiednią bibliotekę. Równocześnie jest to wyrafinowany analizator kodu, który bada aplikację pod względem najczęściej spotykanych błędów. Posiada np. moduł "testowania mutacji" (mutation testing), który sprawdza różne konteksty wywołań. Dzięki niemu można wykryć np. brak deklaracji konstruktora kopiującego, który gdzieś dalej w kodzie jest używany w sposób niejawny. Warto też wiedzieć, że powstało wiele rozwiązań pozwalających monitorować miejsca alokacji i zwalniania konkretnych obiektów. Część firm samodzielnie pisze odpowiednie procedury przesłaniające alokację na stercie i gromadzące informacje w logu.

Można sobie zadać pytanie, czy kod Javy lub. Net zawsze będzie wolny od przecieków pamięci. Błąd typu przepełnienie bufora na pewno nie wystąpi, ale proszę sobie wyobrazić aplikację, która cały czas alokuje pamięć, dodając elementy do listy i nigdy jej nie czyszcząc. W ten sposób mamy klasyczny przeciek pamięci i to taki, który przez długi czas może pozostać niezauważony, ponieważ reszta pamięci będzie "odśmiecana". Co gorsza, w dobie coraz powszechniejszego wykorzystywania bibliotek typu STL, czy innych "pojemników" realizujących podstawowe algorytmy, programista przestaje zajmować się "niskopoziomowym" programowaniem i nie wie lub zapomina o tego typu aspektach aplikacji.

Jeżeli taka biblioteka zawiera błąd, żadne automaty nie pomogą - tu potrzeba człowieka z odpowiednią wiedzą i doświadczeniem. Natomiast dzięki mocnej kontroli typów w Javie i. Net nie ma niebezpieczeństwa, że wskaźnik będzie wskazywać na adres znajdujący się poza dozwolonym obszarem pamięci albo odwoływać się do pola w strukturze, które nie istnieje. Na tym właśnie polega jedna z największych zdobyczy Javy i. Net, bo w języku C, jak pamiętamy, przy zastosowaniu odpowiedniego rzutowania takie niebezpieczne operacje są wykonalne.

Kod zarządzany ma jeszcze jedną zaletę - przed uruchomieniem można sprawdzić, czy aplikacja nie próbuje wykonać niedozwolonej operacji, np. przekroczenia zakresu itp. Ponieważ w środowiskach JVM/MSIL język jest zaprojektowany właśnie pod takim kątem, jest to jak najbardziej możliwe. Dodatkowo można sprawdzić odwołania między typami itp., a co ważniejsze, sporo elementów można zbadać jeszcze zanim zostanie wykonana choćby jedna instrukcja danego bloku programu.

Z drugiej strony, polecam każdemu, kto przez kilka lat programował w środowisku z restrykcyjną kontrolą typów, "odśmiecaczem" pamięci czy innymi udogodnieniami, aby spróbował napisać coś "dla sportu" w C/C++. Wtedy nagle okazuje się, że trzeba myśleć nad wieloma elementami, nad którymi dotychczas nie trzeba było się zastanawiać, ponieważ automatycznie były obsługiwane przez środowisko wykonawcze. Staje się jasne, że większa kontrola nad kodem (poza specyficznymi zastosowaniami) w praktyce niewiele daje, a okazji do popełnienia błędu jest znacznie więcej.

System może... niewiele

W systemach Windows często zdarza się patologia, polegająca na tworzeniu aplikacji, które do poprawnego działania wymagają konta z uprawnieniami administracyjnymi. W systemach z rodziny Unix istnieje możliwość przypisywania praw do aplikacji (w uproszczeniu), co w zasadzie ogranicza ewentualny problem do konkretnej aplikacji, mającej "za duże" możliwości. Dodatkowo występuje tam mechanizm, który pozwala zwykłemu użytkownikowi uruchomić pewne polecenie jako administrator lub inny użytkownik (SUDO). W niektórych dystrybucjach zdarzają się jednak "kwiatki" polegające na tym, że mechanizm SUDO używany jest do skonfigurowania środowiska X-Window, tak by wszystkie bez wyjątku aplikacje uruchamiane były z uprawnieniami administratora - co jest absolutnym zaprzeczeniem idei SUDO.

Operacje związane ze sprzętem wymagają konta root i nic się z tym nie da zrobić. Do tego sam mechanizm SUDO też nie jest pozbawiony błędów - w czerwcu 2005 r. odkryto mechanizm pozwalający na uruchomienie z poziomu SUDO dowolnego (a nie tylko dozwolonego w konfiguracji) polecenia. W Windows także działa podobny mechanizm (Run As), jednak mniej wygodny. Warto podkreślić, że na tej platformie czasami jest tak, iż to program instalacyjny sprawdza, czy użytkownik ma prawa administratora. Program wcale ich nie potrzebuje, ale ponieważ działa jako administrator, może narobić szkód. Dobrze się stało, że obecnie, aby uzyskać logo "Designed for Windows", wymagana jest m.in. możliwość uruchomienia aplikacji w trybie LUA, czyli "nie jako administrator".

Wywołanie kodu procedury tak naprawdę zawsze sprowadza się mniej więcej do tego, że wskaźnik aktualnie uruchamianej instrukcji asemblera jest przenoszony pod wskazany adres i kod jest wykonywany. Jeżeli jest to początek jakiejś procedury, kod ma szansę sprawdzić parametry, długość bufora itp. To jednak teoria - taka operacja i tak zwykle się nie odbywa. Dopiero w ostatnim czasie upowszechnia się sprawdzanie długości bufora. Mimo to i tak nic nie stoi na przeszkodzie, by złośliwy kod "skoczył" do adresu w środku procedury, preparując odpowiednio zawartość stosu. Izolacja procesów częściowo to uniemożliwia, ale... właśnie, częściowo.

Interfejsy API systemu operacyjnego są praktycznie bezbronne. Do tego większość aplikacji pozwala na instalację wtyczek, które realizują "dodatkową funkcjonalność", ale nie sprawdzają "praw" dla tak "wstrzykniętego" kodu - autorzy aplikacji bardzo rzadko umieszczają w aplikacjach funkcje kontroli wtyczek. Wyjątkiem są aplikacje pisane dla środowiska uruchomieniowego, takiego jak Java czy. Net. W tym ostatnim można wprowadzić ustawienie konfiguracyjne, w wyniku którego dynamicznie wczytywana wtyczka nie może wykonać pewnych operacji. Można też wczytać wtyczkę do oddzielnego procesu AppDomain. Java ma mechanizm "piaskownic" (sandbox), w których działa taki kod. Powstał on głównie w celu uruchamiania kodu apletów w przeglądarce internetowej. Jednak tu się pojawia problem praktyczny, bo czasami program czy dodatek musi mieć możliwość zapisania danych na dysku.

Od API zależy wszystko

O radykalnym podniesieniu bezpieczeństwa i stabilności aplikacji tak naprawdę nie można mówić bez zmiany fundamentów projektowania systemów operacyjnych. Oczywiście, działania typu szkolenie programistów, przeprowadzanie audytu/code review itp. będą zmniejszać liczbę takich usterek - ale na pewno ich nie wyeliminują.

Potencjalnie obiecującą koncepcję API systemu operacyjnego pokazuje projekt badawczy Singularity, prowadzony przez Microsoft Research. Jest to system operacyjny oparty na mikrojądrze. Jeśli system operacyjny będzie zbudowany od podstaw na zasadzie restrykcyjnej kontroli typów, wiele błędów zostanie rzeczywiście wyeliminowane. W Singularity zamiast procesów aplikacja (i sam system) składa się z tzw. SIP-ów (Software Isolated Process) - pojemników, w których kod na zewnątrz udostępnia tylko interfejsy. Dodatkowo system dba, by dwa pojemniki nie współdzieliły wskaźnika czy innych zasobów. W sumie chodzi o to, że aplikacja może wykonywać wyłącznie to, na co pozwala jej pojemnik.

Podobną koncepcję stosuje starszy system operacyjny o nazwie Inferno, przeznaczony do tworzenia rozproszonych usług. Jednym z jego elementów jest środowisko uruchomieniowe specjalizowanego języka Limbo, a do komunikacji z systemem wykorzystywany jest protokół o znamiennej nazwie Styx, określający zasady dostępu do interfejsów API i zasobów - z uwzględnieniem współbieżności.

Nie mnożyć ognisk zapalnych

W przypadku dużej firmy programistycznej fragment kodu może być stosowany w bardzo wielu produktach, a czasami można go po prostu skopiować do innego projektu bez tworzenia gałęzi w repozytorium kodu, np. gdy oddzielne repozytoria są dedykowane dla poszczególnych grup. W takim przypadku podstawowym sposobem analizy bezpieczeństwa kodu jest półręczny audyt, bo praca nad potencjalnie błędnie napisanym (a raczej błędnie zaprojektowanym) fragmentem kodu jest wykonywana niezależnie w różnych miejscach. Stąd stworzenie centralnej bazy "błędów" ujednoliconego repozytorium z całością rozwiązań może być pierwszym etapem projektu mającego na celu podniesienie jakości kodu.

Automat pomoże, lecz nie wyręczy

W procesie analizy jakości kodu i poszukiwania błędów bardzo istotne jest odpowiednie planowanie, by to żmudne działanie jak najbardziej usprawnić. Istnieją rozbudowane narzędzia, które potrafią gromadzić zgłaszane błędy, śledzić ich usuwanie itp. Można także zastosować narzędzia pozwalające zapanować nad procesem doskonalenia kodu. Compuware oferuje pakiety CARS (Compuware Application Reliability Solution) oraz QACenter, które (w pewnym uproszczeniu) na podstawie analizy ryzyka pozwalają określać "obszary, w których warto podnieść jakość". Compuware ma także w ofercie pakiet File-AID, umożliwiający przygotowanie danych do testów w środowisku deweloperskim. Podobne narzędzia, z tym że bardziej ukierunkowane na monitorowanie działających już aplikacji, oferuje Mercury. Są to jednak narzędzia przeznaczone głównie do tworzenia i zarządzania testami funkcyjnymi; jeżeli opracowane testy będą mieć za zadanie analizę sytuacji związanych z bezpieczeństwem, to testowane będzie bezpieczeństwo. Niestety narzędzi, które by zarządzały analizą ściśle pod względem bezpieczeństwa, praktycznie nie ma, a dokładniej nie są dostępne publicznie, a jedynie w ramach dużych firm, które własnym sumptem wypracowały je przez lata.


TOP 200