Argante OS

rzyjontko


Przedmowa

Dokument ten został napisany jako referat na ćwiczenia z przedmiotu Systemy Operacyjne i nie należy do zbioru oficjalnej dokumentacji projektu Argante. Autor nie uzgadniał treści poniższego artykułu z żadnym z programistów pracujących nad projektem. Oficjalnej dokumentacji, pełniejszych informacji oraz kontaktu z zespołem należy szukać na oficjalnych stronach projektu pod adresem:

http://www.argante.org.


1 Wstęp

Z informacji prasowej:

25 listopada [2000 roku] udostępniony został kod Argante - nowego, wirtualnego systemu operacyjnego, stworzonego przez grupę polskich programistów. Przy projektowaniu położono ogromny nacisk na bezpieczeństwo oraz efektywność w zastosowaniach rozproszonych, jak niezawodne wirtualne routery, rozproszone clustry czy podobne rozwiązania - ale także na możliwość wykorzystania systemu jako przenośnej i efektywnej platformy do bardziej codziennych zastosowań (grafika, usługi sieciowe).

Sformułowanie tematu niniejszego referatu nie jest przypadkowe. Z premedytacją umieściłem tuż za nazwą Argante litery OS, aby podkreślić, że w pełni uzasadnione jest używanie nazwy ,,Argante Operating System''. Wiele osób miałoby zapewne wątpliwości, czy nie nadużywam terminu ,,system operacyjny'' określając nim jedną z aplikacji systemu Unix. Wymienię więc te cechy Argante, które czynią ten program systemem operacyjnym (posiłkując się przy tym podręcznikiem [1]).

Argante brakuje kilku istotnych cech systemu operacyjnego (przede wszystkim korzysta z procedur bazowego systemu operacyjnego do korzystania z fizycznych zasobów systemu komputerowego), wyposażone jest natomiast w kilka cech, których nie posiadają współcześnie popularne systemy operacyjne. Omówię pokrótce naj-ważniejsze innowacje z punktu widzenia kilku wybranych zadań wypełnianych przez system operacyjny.


2 Zapewnienie Bezpieczeństwa


2.1 Szczypta historii

Pierwsze systemy operacyjne pracowały w trybie wsadowym. Ich jedynym zadaniem była automatyzacja pracy operatora oraz zwiększenie wydajności sprzętu poprzez przyjmowanie do wykonania większej ilości zadań, a następnie udostępnianie im zasobów w miarę potrzeb. W czasie, kiedy jedno z zadań czekało na zakończenie pracy powolnego urządzenia wejścia/wyjścia (np strimera lub drukarki wierszowej) inne mogło swobodnie korzystać z procesora. W systemie takim nie istniało żadne niebezpieczeństwo utraty danych.

Nie było go również w systemach jednostanowiskowych związanych z pierwszymi komputerami osobistymi i systemem operacyjnym MS-DOS (przynajmniej zdaniem jego projektantów). Swobodny dostęp do zasobów komputera okazał się jednak kłopotliwy dla użytkowników. Wielokrotnie zdarzało się bowiem, że nieświadomy swojego błędu programista uruchamiał program, który nadpisywał w pamięci kod systemu operacyjnego.

W procesorach serii 80386 firma Intel wprowadziła dwa tryby pracy procesora: użytkownika i monitora. Komputer jest uruchamiany w trybie monitora i przechodzi do ładowania systemu operacyjnego. System, po zakończeniu inicjalizacji, przekazuje sterowanie użytkownikowi anulując zawczasu uprzywilejowany tryb pracy. Aby program mógł teraz wykonać jedną z potencjalnie niebezpiecznych ope-racji musi zgłosić się do systemu operacyjnego z prośbą o jej wykonanie. System operacyjny przejmuje sterowanie, wykonuje (lub nie) żądaną operację w trybie moni-tora, po czym przełącza się do trybu użytkownika i oddaje sterowanie.

Aby program nie miał bezpośredniego dostępu do pamięci stosuje się różne metody translacji adresów z przestrzeni logicznej na adresy fizycznych bloków pamięci. W ten sposób program nie otrzymuje dostępu do tych obszarów fizycznej przestrzeni adresowej, które nie są dla niego zarezerwowane.

W wielozadaniowych systemach operacyjnych należy dodatkowo odseparować od siebie często złośliwych wobec siebie użytkowników. W systemach Unix wprowadzo-no system kontroli dostępu, w którym z każdym plikiem związanych jest 12 bitów określających prawa do odczytu, zapisu i wykonania danego pliku przez różne grupy użytkowników. O ile sprawdzać się on może na pojedynczych stacjach z wieloma terminalami, o tyle jest trudny do utrzymania w dużych, zdecentralizowanych sieciach lokalnych złożonych z wielu niezależnych jednostek.


2.2 Teraźniejszość

W chwili obecnej obserwujemy bardzo intensywny przyrost liczby komputerów przyłą-czanych do globalnej sieci. Coraz większa ilość domowych maszyn mających stały dostęp do internetu funckcjonuje jako niewielkie, prywatne serwery. Jednocześnie przybywa bezmyślnych wandali, którzy za cel stawiają sobie zniszczenie cudzych danych. Eskalacja tego konfliktu doprowadziła do powstania bardzo wielu mechanizmów ochrony komputera przed atakiem. Mechanizmy te są jednak na ogół bardzo skomplikowane, a często kosztem bezpieczeństwa łamie się standardy sieciowe zawarte w dokumentacji RFC. Administracja niewielkiego serwera staje się zadaniem coraz trudniejszym. Coraz częściej zdarza się, że włamywacze wykorzystują jedynie luki w konfiguracji serwera.


2.3 Dlaczego?

Współczesne systemy nie są odporne na ataki z zewnątrz, bo nie były do tego przystosowane w fazie projektowania. W efekcie trzeba było szukać kompromisu między bezpieczeństwem, a zachowaniem pewnych cech, do których zdążyli się już przyzwyczaić użytkownicy. Dla przykładu rozważmy bardzo popularną konstrukcję języka C.

    while (1){
      accept_connection ();
      if (fork () == 0){
        signal (SIGCHLD, finish_child);
        handle_connection ();
      }
    }

Tak (modulo pewne nieistotne szczegóły) wygląda prawie każdy serwer usług sieciowych. Wygląda tak nie dlatego, że jest to najbardziej wydajna metoda programowania, ale dlatego, że od wielu lat wszyscy programiści tak robią. Tego się naucza na uniwersytetach i politechnikach na całym świecie. Do tego się wszyscy zdążyli przyzwyczaić i nie potrafią już bez tego żyć.

Pozwolę tu sobie na małą dygresję. Core Wars to gra, której akcja rozgrywa się w pamięci pewnego komputera. Do pamięci tej maszyny ładowane są programy napisane przez graczy w abstrakcyjnym języku RedCode. Programy te walczą między sobą starając się między innymi nadpisać swoimi danymi treść programu przeciwnika (próba wykonania danej kończy się w tej grze śmiercią procesu), bądź zmusić go do wykonania niedozwolonej operacji (np dzielenia przez zero). Jednak najciekawszą możliwością uśmiercenia wrażego programu jest nadpisanie fragmentu jego kodu instrukcją fork . W krótkim czasie prowadzi to do powstania tak wielkiej ilości wątków trafionego procesu, że zaczyna on stopniowo zwalniać swoją pracę, aż wreszcie umiera.

Daleki jestem od wyciągania zbyt daleko idących wniosków z gry komputerowej. Faktem jednak jest, że instrukcja fork nie jest instrukcją obojętną dla bezpieczeństwa systemu. Powstały w jej wyniku proces potomny dziedziczy po swoim rodzicu identyfikator użytkownika (uid) i związane z nim uprawnienia, a także kopie deskryptorów otwartych plików. Jeśli ktoś przejmie kontrolę nad procesem, który obsługuje jego połączenie (a trzeba przy tym zauważyć, że wiele serwerów uruchamianych jest z uprawnieniami administratora), to przejmie też kontrolę nad całym systemem operacyjnym.


2.4 Jak?

Przejęcie kontroli nad procesem obsługującym nasze połączenie nie jest łatwe, ale wygląda prawie zawsze tak samo. Przyjrzyjmy się bliżej, jak wygląda na ogół proces słuchający naszych komend.

    char buffer[BUFFER_SIZE];
    close (0);
    close (1);
    dup (socket_descriptor);
    dup (socket_descriptor);
    close (socket_descriptor);
    gets (buffer);
    ... /* command parsing */
    write (answer);

Być może przykład ten jest tendencyjny i mocno uproszczony, ale ilustruje bardzo ogólną sytuację (programistom zdarza się często używać funkcji sscanf i sprintf bez sprawdzenia długości przychodzącego ciągu bajtów) nazywaną popularnie ,,buffer overflow''. Zmienna buffer jest zmienną lokalną, a więc umieszczana jest na stosie wywołań w rekordzie aktywacji razem z adresem powrotu. Zapisanie w tej zmiennej zbyt dużej ilości danych spowoduje nadpisanie adresu powrotu. Przy odrobinie szczęścia możemy zmusić program, aby zamiast powrócić z funkcji skoczył do wyznaczonego przez nas punktu i wykonał spreparowany przez nas fragment kodu. Twórcy systemów operacyjnych godzą się na tę sytuację i proponują pewne rozwiązania zastępcze (np. zabronienie wykonywania instrukcji zapisanych na stosie), które tak naprawdę nie rozwiązują problemu, a tylko komplikują ogólną architekturę systemu operacyjnego.

Aby nie być gołosłownym zamieszczam poniżej przykładowy program przedstawia-ny co roku studentom informatyki pewnej uczelni.

  int main() {
    char komenda[BUFSIZ];

    sprintf(komenda, "ps -fu %s", getenv("LOGNAME"));
    system(komenda);
    return(0);
  }

Ma on co prawda ilustrować działanie uniksowej funkcji bibliotecznej system. Szkoda mimo to, iż wykładowca nie zadbał o to, by przy okazji program ten nie był tak podatny na ataki złośliwych użytkowników. Aby go złamać nie trzeba się wiele napracować. Załóżmy, że program ów został już skompilowany do programu wykonywalnego o nazwie prog1 i umieszczony w katalogu /usr/bin naszego komputera. Przypuśćmy również, że następująca komenda da podobne wyniki.

  user@localhost:/usr/bin$ ls -l prog1
  -rwsr-sr-x    1 root     root         4995 lis 21 01:15 prog1

Wtedy następująca kombinacja poleceń umożliwi nam przejęcie kontroli nad systemem operacyjnym.

  user@localhost:/usr/bin$ export LOGNAME="; /bin/sh"
  user@localhost:/usr/bin$ ./prog1
  sh-2.03# whoami
  root


2.5 Wnioski

,,Where compromise is not the answer.1''

Powyższa sentencja przyświecała autorom przy projektowaniu Argante. Doszli oni zaś do wniosku, że żaden sprzęt i żaden system operacyjny nie jest zaprojektowany w sposób gwarantujący bezpieczeństwo przechowywanych danych. Tak, jak błędne okazało się założenie, że na czterech bajtach będzie można zapisać numery IP wszystkich komputerów przyłączonych do internetu, tak sposób operowania na stosie wywołań oparty jest na błędnych przesłankach. Próby zapobiegania włamaniom na serwery przypominają więc wiązanie sznurkiem złamanej nogi stołu podczas, gdy należałoby ją zastąpić całkiem nową.


2.6 Rozwiązanie

Idealnego rozwiązania nie ma i nie będzie. Nie ulega jednak wątpliwości, że przestarzałe technologie należy z czasem zastępować nowymi - lepiej dopasowanymi do aktualnych warunków. Być może rozwiązanie to nie przyjmie się, ale idee zaprezentowane poniżej będą wykorzystane prędzej lub później.

2.6.1 Sprzęt

Każdy program uruchamiany pod kontrolą Argante otrzymuje do swojej dyspozycji odrębną maszynę wirtualną, a w jej ramach trzy przestrzenie adresowe:

  1. tekst programu
  2. stos wywołań
  3. dane
Tekst programu jest przestrzenią statyczną. Pamięć w obrębie tej przestrzeni jest przeznaczona jedynie do odczytu, a jej rozmiar jest stały przez cały czas działania programu. Jeśli licznik rozkazów przyjmie wartość spoza zakresu adresów bloku tekstu programu, zgłaszany jest wyjątek, a jeśli nie zostanie on obsłużony, to program ulega natychmiastowej terminacji. Uniemożliwia to jakąkolwiek podmianę tekstu programu, jak jest to powszechnie stosowane w innych systemach operacyjnych poprzez wykorzystanie funkcji z rodziny system , popen i exec* .

Stos wywołań jest z punktu widzenia programu abstrakcyjnym typem danym, dla którego określone są tylko dwie operacje: call oraz ret odpowiadające klasycznym operacjom push i pop ale wraz z bezwarunkowym skokiem do odpowiedniego miejsca w tekście programu. Przestrzeń ta jest zupełnie niewidoczna z poziomu programu użytkownika.

Przestrzeń danych jest jedyną częścią pamięci, do której program użytkownika ma nieograniczony i bezwarunkowy dostęp. Dzieli się ona na przestrzeń statyczną, czyli obszar, którego rozmiar jest znany już w momencie załadowania programu do kolejki procesów gotowych do wykonania oraz na przestrzeń dynamiczną. Dostęp do bloków pamięci drugiego rodzaju uzyskuje się po wykonaniu instrukcji alloc , a zrzeka się tego dostępu przy pomocy instrukcji dealloc . Istnieje także możliwość zmiany rozmiaru danego bloku pamięci (służy do tego instrukcja realloc ).

Kolejną innowacją są niskopoziomoe wyjątki. Dzięki temu zmusza się programistę do obsługi wszystkich błędów. Wyjątki współgrają ze stosem wywołań i propagują się na kolejnych poziomach wywołania programu (powodując przy tym automatycz-ne kurczenie się stosu).

2.6.2 Oprogramowanie

W Argante zastosowano hierarchiczny system kontroli dostępu. Samo jądro ma wbudowane dwie istotne funkcje: load_rules oraz is_permitted . Pierwsza z nich ładuje do pamięci zestaw reguł (o tym kawałek dalej). Druga bierze trzy argumenty: numer wirtualnej maszyny, obiekt i operację, po czym sprawdza czy wśród załadowanych reguł znajduje się taka, która zezwala na wykonanie procesowi o określonym numerze domeny danej operacji na danym obiekcie.

Teraz kilka słów o składni pliku zawierającego reguły. Plik ma następujący format (w notacji BNF):

  <plik>::=<linijka>|<plik><linijka>
  <linijka>::=<nr_domeny>:<nr_poddomeny> <obiekt> <operacja> <dostęp>
  <dostęp>::=allow|deny

Rozpatrzmy kilka przykładowych reguł:

  32:0  ipc/source/*/*/115              ipc/stream/recv         allow
  33:0  net/addres/source/tcp/all/1234  net/sock/listen         allow
  34:0  fs/home/rzyj                    fs/fops/open/file/read  allow
  0:0   fs/root                         fs/fops/list/directory  deny

Zarówno nazwa obiektu, jak i nazwa operacji rozpoczyna się od nazwy mo-dułu obsługującego dane operacje i obiekty. Symbol * dopasowuje się do każdego możliwego podstawienia. Numer (sub)domeny 0 oznacza wszystkie procesy danej (sub)domeny. Pierwsza reguła pozwala procesowi o numerze domeny2 32 na odbieranie strumieni danych od procesu znajdującego się w dowolnym wirtualnym systemie, na dowolnej wirtualnej maszynie o numerze rejestracyjnym ipc=115. Druga umożliwia odpowiedniemu procesowi otwarcie do nasłuchu portu tcp o numerze 1234 dla wszystkich przychodzących połączeń. Trzecia zezwala na otwarcie pliku /home/rzyj do czytania. Czwarta zaś zabrania wszystkim procesom sprawdzenia listy plików znajdujących się w katalogu /root.3

Obiekty i operacje nie są zdefiniowane w jądrze, ale w modułach jądra. Dlatego to moduły powinny odpowiadać za sprawdzanie praw dostępu do danego zasobu przed udostępnieniem go. Moduły odpowiadają także za semantykę i szczegółowość poszczególnych operacji. Możliwe jest oczywiście napisanie modułu, który udostępnia zasoby dyskowe lub połączenia sieciowe nie zwracając uwagi na związane z tym niebezpieczeństwa. Argante udostępnia bowiem interfejs do tworzenia modułów, które udostępniają programom użytkownika wywołania systemowe. Interfejs taki został stworzony w celu dosyć prostego wzbogacania funkcjonalności systemu, ale użytkownik musi być świadomy tego, co robi i brać za to odpowiedzialność. Sam system udostępniany jest z kilkoma predefiniowanymi modułami, które są napisane w sposób gwarantujący, że żaden program nie naruszy reguł bezpieczeństwa.


3 Wieloproprogramowość

Współczesne procesory działają niezwykle szybko. Podobno ilość operacji, jakie potrafi wykonać procesor w ciągu sekundy, podwaja się co kilka miesięcy. Aby zapobiec sytuacji, w której moc obliczeniowa jednostki arytmetyczno-logicznej jest niewykorzystana, systemy operacyjne umożliwiają zamienne wykonywanie się kilku procesów równolegle. W czasie, gdy jeden z procesów oczekuje na reakcję użytkownika, inny może wykonać kilkaset milionów operacji. Istnieje kilka sposobów na zapewnienie równoległego wykonywania kilku programów.


3.1 Procesy ciężkie

Jest to historycznie najstarsze podejście do zagadnienia wieloprogramowości. W mo-delu takim system zajmuje się zarządzaniem wszystkimi procesami. Odpowiada za powstawanie nowych, usuwanie zakończonych oraz za przełączanie kontekstu między wykonywanymi procesami. Z każdym procesem związanych jest kilka lub kilkanaście wartości, które trzeba podczas operacji przełączania kontekstu zachować. Są to stany rejestrów, adres stosu, lista otwartych deskryptorów plików i wiele innych. Przełączanie kontekstu jest więc w takiej sytuacji dość czasochłonne.


3.2 Wątki poziomu jądra

Czasami programista potrzebuje napisać program, który będzie składał się z kilku logicznie wyodrębnionych części wykonywanych równolegle. Aby zapobiec kopiowaniu całej przestrzeni adresowej procesu macierzystego (chociaż w nowoczesnych systemach operacyjnych unika się takiej sytuacji poprzez tzw. copy-on-write) i ułatwić współdzielenie fragmentom programu wspólnych danych wprowadza się wątki. Podczas każdego przełączenia kontekstu między dwoma wątkami tego samego procesu nie ma potrzeby zachowywania i odtwarzania wielu wspólnych danych. Rozumiane w ten sposób wątki są zaimplementowane w większości współczesnych systemów operacyjnych (np fragmentem standardowej biblioteki C w systemie GNU/Linux jest nagłówek pthread).


3.3 Wątki poziomu użytkownika

W systemie komputerowym procesy rywalizują o dostęp do procesora. System operacyjny jest jednak odpowiedzialny za to, aby żaden z nich nie został ,,zagłodzony''. Ponieważ istnieje możliwość zapętlenia się programu na zawsze system operacyjny ustawia sprzętowy zegar, który po określonym kwancie czasu zgłasza sprzętowe przerwanie, przekazując w ten sposób sterowanie do systemu, który może przełączyć kontekst. Wątki jednego procesu nie rywalizują jednak o dostęp do procesora, tylko na ogół kooperują. Nie istnieje więc potrzeba odbierania im tego zasobu zanim skończą z niego korzystać. Ekspedytor może zatem działać zgodnie z innym algorytmem (np priorytetowym). Ponadto przełączanie kontekstu między takimi wątkami może być znacznie szybsze, gdy się je uniezależni od systemu operacyjnego. W skrajnych przypadkach może ono polegać wyłącznie na wykonaniu pojedynczej instrukcji długiego skoku4 realizowane są przez odrębną względem systemu operacyjnego bibliotekę (np uniksową pth) i wymagają od programisty, aby co jakiś czas każdy z jego wątków zrzekał się sterowania na rzecz innego, bądź wykonywał jedną z operacji bibliotecznych, które go tego sterowania pozbawią.


3.4 Argante

W Argante każdy proces jest obiektem statycznym. System wybiera pierwszą wolną maszynę wirtualną i ładuje program do jej pamięci. Każda z maszyn wirtualnych ma przyporządkowaną liczbę określającą ilość rozkazów wykonywanych w trakcie każdego cyklu5. System na zmianę przekazuje sterowanie kolejnym maszynom, których procesy są gotowe do wykonania. Nie istnieje więc możliwość duplikacji działającego procesu, a ilość potrzebnych wątków, które mają współpracować przy wykonywaniu jednego zadania musi być znana podczas ładowania programu.


3.5 Przykład

Rozważmy implementację przykładowego programu - serwera usługi FTP. Przykład ten pochodzi z [2]. Przez model klasyczny rozumiem tutaj jeden z powszechnych modeli wieloprocesowości omówionych powyżej. Poniżej korzystam jedynie z terminu proces, ale dotyczy to jednakowo wątków.


3.5.1 Model klasyczny

W modelu tym istnieje tylko jeden program usługi ftp. Program ten nasłuchuje w nieskończonej pętli, czy na wskazany port nie przychodzi zgłoszenie z zewnątrz. Gdy do portu dotrze pakiet serwer rozwidla się i proces potomny przejmuje połączenie. Proces macierzysty kontynuuje zaś nasłuchiwanie na porcie i powtarza swoje postępowanie analogicznie dla następnych połączeń. Proces potomny przeprowadza zaś autoryzację, po czym dokonuje analizy składniowej poleceń i udostępnia dane dyskowe klientowi usługi ftp.


3.5.2 Argante

Potrzebnych jest kilka współpracujących ze sobą procesów.

A
Proces obsługujący przychodzące połączenia.
B
Proces dokonujący analizy syntaktycznej poleceń.
C
Proces przeprowadzający autoryzację.
D
Proces czytający i zapisujący na dysk.

Będziemy przy tym potrzebować conajmniej kilku procesów typu B. Pozostaje tylko dobrze zaprojektować reguły dostępu. Proces A będzie więc mógł jedynie nasłuchiwać na porcie oraz wysyłać komunikaty do procesów B (będą one miały wspólny numer rejestracyjny ipc). Każdy z procesów B będzie mógł tylko porozumiewać się z procesem C. Ten zaś, będzie miał dostęp jedynie do bazy danych użytkowników w celu przeprowadzenia autoryzacji. Będzie w ten sposób pośredniczył między procesem B a D, który jako jedyny będzie mógł czytać i zapisywać dane na dysku.

Poniżej zamieszczam schemat pochodzący z [2].

Rysunek 1: schemat serwera ftp
\begin{figure}\begin{verbatim}TCP/IP database user files
\vert \vert \vert ''...
...t user space
<A>-+ <B> <B> <B>-+ <C>-+-<D>\end{verbatim} \end{figure}


3.6 Porównanie

Niewątpliwie rozwiązanie klasyczne jest znacznie prostsze w implementacji. Ma to jednak swoje konsekwencje. Przejęcie kontroli nad procesem, który obsługuje połączenie może doprowadzić do uszkodzenia danych na dysku. W drugim modelu nawet przejęcie kontroli nad wątkiem obsługującym połączenie nie doprowadzi do żadnych konsekwencji, ponieważ jedyną dozwoloną akcją jest ,,rozmowa'' przez kanały ipc z procesem C.

Literatura

1
Silbershatz A., Galvin P. B.: Podstawy Systemów Operacyjnych. WNT, Warszawa 1993, 2000

2
Zalewski M., Skura A.: README. 2000, 2001

About this document ...

Argante OS

This document was generated using the LaTeX2HTML translator Version 99.2beta6 (1.42)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -split 0 -nonavigation -show_section_numbers -footnode argante.tex

The translation was initiated by rzyjontko on 2001-11-25


Footnotes

... answer.1
Tam, gdzie kompromis nie jest rozwiązaniem.
... domeny2
Numer ten jest ustalany w trakcie ładowania programu do pamięci i jest stały do końca działania programu.
... /root.3
Plik /home/rzyj oraz katalog /root mogą mieć zupełnie inną rzeczywistą lokalizację na dysku. Więcej informacji na temat modułu fs znajduje się w [2].
... skoku4
Chodzi tu o funkcję longjmp zdefiniowaną w standardach ISO/ANSI języka C.
... cyklu5
Liczba ta jest ustalana podczas ładowania programu.


rzyjontko 2001-11-25