Plik readme do projektu FileGuard Rafał Wijata Ten plik opisuje czym są, i jak używać rozszerzenia FG pod linuxem. Jak na razie działa tylko dla systemu plików ext2, ale może łatwo być przeniesiony na ReiserFS, choć z nazwami tylko 2 bajtowymi. Jest to związane z faktem, iż tylko tyle bajtów można wykożytać na własne cele. Dla ext2 są dostępne dwa pola 4bajtowe (dla architektury x86). Wstęp System plików dostarczany z systemem operacyjnym nie bardzo nadaje się do użycia do specjalnych celów jakie moglibyśmy sobie postawić. Wszystkie pliki (i katalogi) są traktowane w taki sam sposób bez wyjątku. Jesli jednak chcielibyśmy aby jakis plik był obsługiwany w inny sposób niż pozostałe to nie mamy na szans. To oznacza, że systemy plików, takie jak je znamy, nie są w ogóle elastyczne i nie pozwalają użytkownikom na traktowanie plików w ich własny sposób. Dla przykładu - jeśli chcielibyśmy aby nasz plik .plan w czasie pracy zawierał kontakt do firmy, a w czasie wolnym do domu lub na komórkę, to nie bardzo mamy jak to zrobić. Jeśli chcielibyśmy aby użytkownik z pliku passwd mógł zobaczyć tylko linię z własnym wpisem to rownież nie bardzo możemy. Na te problemy pomoże moja implementacja FG. Jest to kod, który większość operacji na plikach potrafi wynieść poza jadro systemu operacyjnego, tak aby zwykłe programy mogły je wykonywać w imieniu jądra. Ponieważ użycie programów z przestrzeni użytkownika powoduje duże opóźnienia, również (a może przede wszystkim) jako strażniki mogą działać moduły jądra. FileGuard - czyli straznik pliku, jest podpiety do metryczki pliku permanentnie, czyli jest zapisany na dysku tak samo jak właściciel pliku, czy prawa dostępu. W metryczce jest zapisana nazwa (ciąg znaków) strażnika. W momencie otwierania pliku strażnik, jeśli jest zarejestrowany w systemie, będzie wykonywał operacje na pliku zamiast jądra. Jak to działa? Po pierwsze należy plik oznaczyć jako pilnowany(strzeżony). Do tego służy dodatkowa funkcja systemowa sys_fglink(const char *filename, char *fg). Jej parametry to filename, czyli nazwa pliku na którym chcemy operaować. Drugi parametr to nazwa straznika. W tej chwili moze ona mieć do 4 znaków. Jeśli podany zostanie dłuższy ciąg znaków, tylko pierwsze cztery znaki będą miały znaczenie. Te cztery znaki zostaną zapisane w metryczce pliku systemu ext2 w zarezerwowanym 4bajtowym polu o nazwie ext2_inode.linux1.l_i_reserved1 (patrz plik include/linux/ext2_fs.h). Jesli drugim parametrem bedzie pusty ciąg znaków funkcja skopiuje do niego aktualną nazwę(czytanie nazwy strażnika), a jesli to będzie NULL nazwa strażnika zostanie usunięta. Przykładowe użycie funkcji: #include #include _syscall2(int, fglink, const char *, filename, char *, fg) int i; if (i = fglink("/etc/passwd", NULL)) { fprintf(stderr, "fglink failed with error=%d\n", i); } else { char fg[9]; fg[0] = 0; fglink("/etc/passwd", fg); printf("fglink OK, now is %s\n", fg); }; Co się stanie jednak jeśli ktoś spróbuje otwożyć pilnowany plik, a w systemie nie ma zarejestrowanego strażnika o danej nazwie? Po pierwsze system spróbuje załadować strażnika wykonując polecenie "/sbin/modprobe fg_nazwa". Jeśli to zawiedzie, zostanie sprawdzona specjalna zmienna fg_allow, jesli jest ustawiona plik zostanie obsłużony tak jak zwykły (nie strzeżony), wpp. dostęp zostanie zabroniony. W przypadku superużytkownia zmienna nie ma znaczenia i dostęp będzie udzielony. Wartość zmiennej można odczytać/ustawić poprzez plik /proc/fs/fg_allow. W tym katalogu jest jeszcze plik fg_stat, z którego można odczytać nazwy aktualnie zarejestrowanych strażników. Jak zatem strażnik może sie zarejestrować? Służy do tego funkcja register_fgfunc(char *name, struct fgf_operations *fgops). Name to nazwa strażnika, a struktura fgops to struktura wskaźników na funkcje realizowane przez tego strażnika. Definicja tej struktury jest w pliku include/linux/fg.h. Możliwe do zaimplementowania funkcje to: loff_t (*llseek) (struct file *, loff_t, int) ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long int (*mmap) (struct file *, struct vm_area *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); int (*permission) (struct inode *, int); Wszystkie, oprócz ostatniej są funkcjami z file_operations. Ostatnia jest funkcją z inode_operations i odpowiada ona na pytania typu: permission(ino, MAY_WRITE); W strukturze jest jeszcze pole "struct module *owner", które MUSI być wypełnione wskaźnikiem do modułu, który rejestruje strażnika. Służy to potem do blokowania modułu w pamięci tak, aby użytkownik nie mógł go odładować jeśli jakiś pilnowany przez niego plik jest otwarty. Do odrejestrowania strażnika służy funkcja unregister_fgfunc(char *name, struct fgf_operations *fgops). Jak to jest zaimplementowane? Zacznijmy od funkcji permission(). W pliku fs/namei.c w definicji funkcji jeśli plik jest pilnowany, wołana jest funkcja-wrapper fg_permission(). Ta jest zdefinowana w pliku fs/fg/fg_main.c. Ta jeśli strażnik jest zarejestrowany i obsługuje funkcję permission() woła ją i zwraca jej wynik. W przeciwnym wypadku zwraca błąd -EAGAIN. Ten kod błędu jest specjalny i oznacza, iż należy zawołać funkcję, ktora została zaimplementowana w jadrze standardowo. Tak tez się dzieje. Fakt ten oznacza, że strażnik może implementować funkcje, które robią coś nie związanego z samym procesem sprawdzania praw dostępu, a następnie jako wynik zwrócić -EAGAIN, co zaowocuje w sprawdzeniu unixowych praw dostępu. Ten mechanizm działa dla wszystkich funkcji realizowanych przez strażnika. Kolejną funkcją jest open() z pliku fs/open.c. Tu również, jeśli plik jest pilnowany, wołany jest wrapper fg_open(). Funkcja woła funkcję zdefinowaną w strażniku (jeśli jest), następnie jeśli trzeba woła oryginalny open() systemowy. Jeśli procedura otwarcia pliku się powiedzie, zastępuje file_operations dla tej instancji otwarcia pliku na własną strukturę wypełnioną wskaźnikami do wrapperów napisanych w fg_main.c. Struktura nazywa się fg_default_fops. Od tej pory będą wołane funkcje zdefiniowane w fg_main.c a nie standardowe file_operations dla danego systemu plików. Każda z funkcji-warppera robi tak na prawdę to samo co fg_open(). Zatem najpierw sprawdza czy strażnik obsługuje tę funkcję. Jeśli tak - woła ją, jeśli nie zakłada, że zwraca ona -EAGAIN. Jeśli wynikiem jest -EAGAIN, woła oryginalną funkcję dla pliku (oryginalna struktura file_operations jest zapamietywana na polu f_op_fg. Patrz include/linux/fs.h:struct file_operations). Resultat jest zwracany do wołającego funkcję systemową. I to właściwie cały mechanizm. Co zatem należy napisać w strażniku? Funkcje systemowe typu open(), read(), write() etc. Umieścić je w strukturze fgf_operations, zarejestrować strażnika, i to już wszystko. Przykładowe moduły można znaleźć w katalogu fs/fg/. Należy tu jeszcze napisać co może zrobić taka funkcja strażnika. 1. może jej nie być(wartość wskaźnika równa NULL)! Wtedy zostanie zawołana funkcja standardowa dla danego systemu plików. 2. może nie robić nic ciekawego z punktu widzenia jądra systemu, zatem może np. odnotować w logu dostęp do pliku, a następnie zwrócić -EAGAIN, czyli poprosić jądro o wykonanie operacji w standardowy sposób. 3. może wykonać operację i zwrócić wynik (lub błąd). 4. może zawołać oryginalną funkcję [file->f_op_fg->read()], zmodyfikować wyniki, i zwrócić (może zmodyfikowany) kod wykonania operacji. Moduł dostępu programów z przestrzeni użytkownika. Na początku napisałem, że programy użytkownika również mogą działać jako strażnicy. Aby to było możliwe, trzeba im zdefinować jakiś protokół i sposób komunikacji z wyżej opisanym mechanizmem. Zrobione jest to poprzez specjalny moduł jądra (fs/fg/fg_user.c). Jest on pomostem między programami użytkownika i jądrem. To właśnie ten moduł będzie rejestrował strażników w imieniu programów, i będzie przesyłał do nich żądania jądra. Najpierw jednak program użytkownika musi się zarejestrować jako strażnik w module użytkownika sam. Robi to otwerając urządzenie /dev/fgu0 (minor == 0). Podczas procedury otwarcia zostanie zarezerwowany kanał połączenia z jądrem dla procesu. Numer kanału proces otrzyma jako komunikat odczytany z otwartego urządzenia. Jeśli numerem kanału jest N, proces musi teraz otworzyć urzadzenie /dev/fguN (minor == N), i wysłać komunikat rejestrujący strażnika. Komunikat zawiera nazwe strażnika oraz liste operacji jakie będzie on obsługiwał. Tę listę odzwierciedla struktura fgu_operations zdefiniowana w pliku fg.h. Każde pole jest typu int i może mieć wartość: * FGU_MYSELF(0) - funkcja obsługiwana * FGU_WDATA(1) - funkcja obsługiwana, prośba o wcześniejsze wykonanie funkcji standardowej i podesłanie wyników do obróbki * FGU_NORMAL(-EAGAIN) - normalne wykonanie funkcji z jądra * FGU_REJECT(-EPERM) - zabroniony dostęp * wartość < 0 - błąd zwracany uzytkownikowi. Protokół komunikacji jest dość restrykcyjny. Wymaga konkretnej kolejności pakietów i ich rozmiaru. W przypadku złamania protokołu zwracany jest błąd, który zazwyczaj w dalszej części wymiany danych owocuje zerwaniem kanału i wyrejestrowaniem strażnika. Po otrzymaniu komunikatu rejestrującego, moduł sam rejestruje w jądrze strażnika o takiej samej nazwie, podając własne funkcje-wrappery jako wypełnienie struktury fgf_operations. Od tej pory program użytkownika działa w systemie jako strażnik. Żądania systemowe (read, write...) są przekierowywane do modułu, tez zaś usypia proces zgłaszający żądnie, i wysyła komunikat z prośbą o wykonanie go do procesu-strażnika. Po otrzymaniu odpowiedzi kopiuje ją do procesu żądającego i przywraca go do działania. Proces-strażnik może kożystać z obsługiwanych przez siebie plików, omijając mechanizm strażników. Nie musi do tego stosować żadnych dodatkowych funkcji - moduł sam zapewnia, aby strażnik nie został wywołany dla obsługi samego siebie. Działa to jednak tylko w przypadku, gdy proces korzystający z tego mechanizmu jest tym samym procesem (pid), który otwierał urządzenie /dev/fgu0. W innym przypadku nie ma jak sprawdzić czy jest to proces-strażnik, czy nie. Sposób komunikacji procesu z modułem: Na początek warto wiedzieć jak wygląda komunikat: struct fgu_mesg { int magic; /* our magic numer */ int cmd; /* command */ unsigned int no; /* task number */ int sid; /* session id */ pid_t pid; /* pid requesting oper */ int len; /* length of data following */ }; * magic zawsze musi być ustawiony na FGU_MAGIC * cmd jest kodem komunikatu, kody zdefinowane są w pliku fg.h. Np. FGU_READ * no jest kolejnym numerem komunikatu, odpowiedź na komunikat musi mieć ten sam numer * sid jest identyfikatorem sesji, potrzebne w przypadku obsługi wielu plików na raz w procesie * pid jest pidem procesu zgłaszającego żądanie, dzięki temu strażnik może dowiedzieć się kto prubuje dostać się do pliku. * len jest długością danych nastepujących po strukturze. Te dane są różne dla różnych komunikatów. I tak: FGU_LSEEK: int new_pos int origin -> odpowiedź to: int new_pos FGU_READ: int count int f_pos -> odpowiedź to: int count int new_f_pos void data[count] FGU_WRITE: int count int f_pos void data[count] -> odpowiedź to: int count int new_f_pos FGU_READDIR: nie obsługiwane FGU_POLL: nie obsługiwane FGU_IOCTL: int command int argument -> odpowiedź to: int return_code FGU_MMAP: nie obsługiwane FGU_FLUSH: brak danych -> odpowiedź to: int return_code FGU_FSYNC: int datasync -> odpowiedź to: int return_code FGU_FASYNC: int on_what -> odpowiedź to: int return_code FGU_LOCK: nie obsługiwane FGU_OPEN: int return_code (oryginalnego wywołania, jeśli było) char *filename -> odpowiedź to: int return_code FGU_RELEASE: brak danych -> odpowiedź to: int return_code (ignorowany, prawdziwy kod funkcji close() da funkcja flush() wołana zawsze przy zamykaniu pliku) FGU_PERMISSION: int requested_mask -> odpowiedź to: int return_code Warto dodać, że proces zgłaszający żądanie zostaje uśpiony na określony czas (3 sekundy), w czasie których strażnik musi zdążyć wykonać zadanie. W przeciwnym przypadku do procesu zwracany jest kod błędu -ERESTART. Ma to zapobiec uśpieniu wszystkich procesów w systemie w przypadku błędnie działającego strażnika (np. pilnującego pliku etc/passwd). Przykładowi strażnicy są w Documentation/fg/cryp.c i date.c