Spis treści

FUSE - Wirtualne systemy plików w przestrzeni użytkownika

Wstęp

W normalnych warunkach dodanie obsługi nowego typu systemu plików wymaga stworzenia i załadowania specjalnie do tego przeznaczonego modułu jądra. Ponadto, do montowania systemów plików zazwyczaj uprawniony jest tylko użytkownik root. Jednak interfejs VFS (ang. Virtual File System) jądra coraz częściej z powodzeniem wykorzystuje się do zastosowań innych, niż zarządzanie danymi przechowywanymi na urządzeniach blokowych. Wobec tych zmian pojawia się potrzeba z jednej strony umożliwienia zwykłym użytkownikom montowania systemów plików i ładowania do nich sterowników na własny użytek i odpowiedzialność, z drugiej - ułatwienia pisania sterowników dla takich systemów nieopartych na urządzeniach blokowych. Framework Filesystem in Userspace (FUSE) jest jedną z możliwości realizacji tych potrzeb.

Zasada działania

FUSE składa się z dwóch głównych komponentów:

W celu dodania możliwości rozwijania swoich własnych systemów plików należy wykonać jedną z poniższych czynności:

Systemy plików oparte na FUSE obsługiwane są przez proces działający w przestrzeni użytkownika (tzw. filesystem daemon). Każdy system plików jest obsługiwany przez osobny daemon. Właścicielem procesu-daemona jest użytkownik, który zamontował obsługiwany przez niego system plików (a więc nie musi to być root) :!:

Warto zwrócić uwagę, że w jednej chwili może być uruchomionych wiele daemonów obsługujących systemy plików tego samego typu, nawet z tym samym właścicielem. Operacje na systemie plików obsługiwane przez daemona są dostępne przez interfejs VFS jadra dzięki modułowi fuse.ko. Komunikacja pomiędzy modułem fuse.ko a kodem obsługującym system plików jest realizowana przez bibliotekę libfuse.

Przepływ żądania przy przykładowej operacji na systemie plików opartym na FUSE dobrze ilustruje poniższy obrazek (w tym przykładzie plik example/hello jest daemonem obsługującym system plików zamontowany w katalogu /tmp/fuse):

Bezpieczeństwo

Umożliwienie nieuprzywilejowanym użytkownikom tworzenia i montowania własnych systemów plików może pociągać za sobą wiele zagrożeń: dla całego systemu lub innych użytkowników. Najbardziej oczywistym zapobiega się przez:

Implementacja w C

FUSE definuje listę operacji możliwych do wykonania w ramach implementowanego systemu plików. Lista tych operacji jest przedstawiona w automatycznie wygenerowanej dokumentacji do biblioteki, którą można znaleźć tutaj. Wszystkie z powyższych operacji są zwykłymi wskaźnikami do funkcji, które zostały zgromadzone w strukturze fuse_operations. W większości ich deklaracja oraz semantyka jest podobna lub identyczna do standardowych funkcji operujących na plikach. Wyjątkiem jest przekazywanie informacji o błędach: Zamiast ustawiać odpowiednią wartość zmiennej errno, funkcje powinny zwrócić wartość ujemną, równą co do wartości bezwzględnej kodowi błędu.

Implementacja systemu plików sprowadza się do:

  1. zaimplementowania wybranych funkcji,
  2. utworzenia zmiennej typu struct fuse_operations, w której ustawione zostaną wskaźniki odpowiadające zaimplementowanym funkcjom,
  3. wywołania funkcji fuse_main:
    fuse_main(argc, argv, &my_ops);
    • pierwsze dwa argumenty są analogiczne do wywołania funkcji main,
    • ostatni wskazuje na wartość struktury fuse_operations.
  4. skompilowanie programu i zlinkowanie go go z biblioteką libfuse:
    gcc hello.c `pkg-config fuse --cflags --libs` -o hello

    Do tego celu należy mieć zainstalowany pakiet pkg-config.

Instrukcje do wykonania

1 HelloWorld

  1. Prześledzić, pobrać poniższy program:
    hello.c
    #define FUSE_USE_VERSION 26
     
    #include <fuse.h>
    #include <stdio.h>
    #include <string.h>
    #include <errno.h>
    #include <fcntl.h>
     
    static const char *hello_str = "Hello World!\n";
    static const char *hello_path = "/hello";
     
    static int hello_getattr(const char *path, struct stat *stbuf) {
        int res = 0;
     
        memset(stbuf, 0, sizeof(struct stat));
        if (strcmp(path, "/") == 0) {
            stbuf->st_mode = S_IFDIR | 0755;
            stbuf->st_nlink = 2;
        } else if (strcmp(path, hello_path) == 0) {
            stbuf->st_mode = S_IFREG | 0444;
            stbuf->st_nlink = 1;
            stbuf->st_size = strlen(hello_str);
        } else
            res = -ENOENT;
     
        return res;
    }
     
    static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                 off_t offset, struct fuse_file_info *fi) {
        (void) offset;
        (void) fi;
     
        if (strcmp(path, "/") != 0)
            return -ENOENT;
     
        filler(buf, ".", NULL, 0);
        filler(buf, "..", NULL, 0);
        filler(buf, hello_path + 1, NULL, 0);
     
        return 0;
    }
     
    static int hello_open(const char *path, struct fuse_file_info *fi) {
        if (strcmp(path, hello_path) != 0)
            return -ENOENT;
     
        if ((fi->flags & 3) != O_RDONLY)
            return -EACCES;
     
        return 0;
    }
     
    static int hello_read(const char *path, char *buf, size_t size, off_t offset,
                  struct fuse_file_info *fi) {
        size_t len;
        (void) fi;
        if(strcmp(path, hello_path) != 0)
            return -ENOENT;
     
        len = strlen(hello_str);
        if (offset < len) {
            if (offset + size > len)
                size = len - offset;
            memcpy(buf, hello_str + offset, size);
        } else
            size = 0;
     
        return size;
    }
     
    static struct fuse_operations hello_oper = {
        .getattr    = hello_getattr,
        .readdir    = hello_readdir,
        .open       = hello_open,
        .read       = hello_read,
    };
     
    int main(int argc, char *argv[]) {
        return fuse_main(argc, argv, &hello_oper, NULL);
    }
  2. Skompilować program poleceniem:
    gcc hello.c `pkg-config fuse --cflags --libs` -o hello
  3. Zamontować system plików na dwa sposoby:
    1. Jak zwykły użytkownik:
      • mkdir hellofs
      • ./hello hellofs
    2. Jako root:
      • mount -t fuse hello hellofs
  4. W każdym przypadku spróbować wejść (cd) do katalogu hellofs i wyświetlić listę  plików (ls). Można też spróbować użyć do tego celu managera plików nautilus.
  5. Proszę spróbować przeczytać plik hello z katalogu hellofs.
  6. Odnaleźć w kodzie programu miejsca gdzie zdefiniowane zostały:
    • Nazwa pliku hello.
    • Prawa dostępu do katalogu i pliku.
    • Odczyt zawartości katalogu.
    • Odczyt zawartości pliku.
  7. Odmontować system plików:
    1. Jako zwykły użytkownik:
      • fusermount -u hellofs
    2. Jako root:
      • umount hello

2 Własny system plików

Celem tego ćwiczenia będzie stworzenie własnego, prostego systemu plików, którego zadaniem będzie pobieranie informacji o systemie. Działanie systemu plików będzie następujące:

Ćwiczenia

Ćwiczenie 1 - Analiza programu
  1. Pobierz poniższy program oraz komplementarne do niego pliki:
    myfs-main.c
    #define FUSE_USE_VERSION 26
    /* ---------------------------------------------------------------------------- */
     
    #include <fuse.h>
    #include <stdio.h>
    #include <string.h>
    #include <errno.h>
    #include <fcntl.h>
     
    #include "myfs_tools.h"
    /* ---------------------------------------------------------------------------- */
     
    static const char *hello_str = "Hello World!\n";
    /* ---------------------------------------------------------------------------- */
     
    static int myfs_getattr(const char *path, struct stat *stbuf) {
        int res = 0;
     
        memset(stbuf, 0, sizeof(struct stat));
        if (strcmp(path, "/") == 0) {
            stbuf->st_mode = S_IFDIR | 0755;
            stbuf->st_nlink = 2;
        } else if(myfs_file_exists(path)) {
            char* fullpath = myfs_file_full_path(path);
            stat(fullpath, stbuf);
            free(fullpath);
        } else
            res = -ENOENT;
     
        return res;
    }
    /* ---------------------------------------------------------------------------- */
     
    static int myfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                 off_t offset, struct fuse_file_info *fi) {
        (void) offset;
        (void) fi;
     
        if (strcmp(path, "/") != 0)
            return -ENOENT;
     
        filler(buf, ".", NULL, 0);
        filler(buf, "..", NULL, 0);  
     
        // open dir
        char* dirname = myfs_dir_get_fuse_home_path();
        DIR* dirptr = myfs_dir_open(dirname);
        free(dirname);
        if(dirptr == NULL)
            return -errno;
     
        // read dir content
        while(1) {
            struct dirent * entry = myfs_dir_read(dirptr);
            if(!entry) break;
            filler(buf, entry->d_name, NULL, 0);
        }
     
        // close dir
        myfs_dir_close(dirptr);
     
        return 0;
    }
    /* ---------------------------------------------------------------------------- */
     
    static int myfs_open(const char *path, struct fuse_file_info *fi) {
     
        if(myfs_file_exists(path) == 0)
            return -ENOENT;
     
        if ((fi->flags & 3) != O_RDONLY)
            return -EACCES;
     
        return 0;
    }
    /* ---------------------------------------------------------------------------- */
     
    static int myfs_read(const char *path, char *outputbuf, size_t size, off_t offset,
                  struct fuse_file_info *fi) {
     
        pid_t pid;
        int pfd[2], i;
        char offbuf[1];
        ssize_t nread;
        ssize_t totalread = 0;
     
        (void) fi;
     
        pipe(pfd);
        pid = fork();
     
        // child process
        if(pid == 0) {
            dup2(pfd[1],STDOUT_FILENO);
            close(pfd[0]);
            close(pfd[1]);
            char* fullpath = myfs_file_full_path(path);
            execlp(fullpath, fullpath, NULL);
            // on error
            close(STDOUT_FILENO);
            exit(0);
        }
     
        // parent process
        else if(pid > 0) {
            close(pfd[1]);
            for(i=0;i<offset;++i)
                if(read(pfd[0], offbuf, 1) == 0)
                    break;
            do {
                nread = read(pfd[0], outputbuf+totalread, size-totalread);
                totalread += nread;
            }
            while((nread != 0) && (totalread < size));
            close(pfd[0]);
        }
     
        else {
            return -EACCES;
        }
     
        return totalread;
    }
    /* ---------------------------------------------------------------------------- */
     
    static struct fuse_operations myfs_oper = {
        .getattr    = myfs_getattr,
        .readdir    = myfs_readdir,
        .open       = myfs_open,
        .read       = myfs_read,
    };
    /* ---------------------------------------------------------------------------- */
     
    int main(int argc, char *argv[]) {
     
        return fuse_main(argc, argv, &myfs_oper, NULL);
    }
    /* ---------------------------------------------------------------------------- */
    myfs_tools.c
    #include "myfs_tools.h"
     
    /* ---------------------------------------------------------------------------- */
     
    char* myfs_dir_get_fuse_home_path() {
        struct passwd *pw = getpwuid(getuid());
        char* result;
        if(pw == NULL)
            return NULL;
     
        result = (char*)malloc(sizeof(char)*(strlen(pw->pw_dir))+1+5);
        strcpy(result, pw->pw_dir);
        strcat(result, "/fuse");
        return result;
    }
    /* ---------------------------------------------------------------------------- */
     
    char* myfs_file_full_path(const char* fusepath) {
        char* dirname = myfs_dir_get_fuse_home_path();
        char* fullpath = (char*)malloc(sizeof(char)*(strlen(dirname) + strlen(fusepath) + 1));
        strcpy(fullpath, dirname);
        strcat(fullpath, fusepath);
        free(dirname);
     
        return fullpath;
    }
    /* ---------------------------------------------------------------------------- */
     
    int myfs_file_exists(const char* fusepath) {
        char* fullpath = myfs_file_full_path(fusepath);
        int result = (access(fullpath, F_OK) != -1) ? 1 : 0;
        free(fullpath);
     
        return result;
    }
    /* ---------------------------------------------------------------------------- */
     
    DIR* myfs_dir_open(char* dir_name) {
     
        return opendir(dir_name);
    }
    /* ---------------------------------------------------------------------------- */
     
    struct dirent* myfs_dir_read(DIR* d) {
     
        return readdir(d);
    }
    /* ---------------------------------------------------------------------------- */
     
    int myfs_dir_close(DIR* d) {
     
        return closedir(d) == 0;
    }
    /* ---------------------------------------------------------------------------- */
    myfs_tools.h
    #ifndef MYFS_TOOLS_H
    #define MYFS_TOOLS_H
    /* --------------------------------------------------- */
     
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/types.h>
    #include <dirent.h>
    #include <string.h>
    #include <errno.h>
    #include <unistd.h>
    #include <pwd.h>
    /* --------------------------------------------------- */
     
    /* Tworzy sciezke do plikow wykonywalnych dla obecnego uzytkownika */
    char* myfs_dir_get_fuse_home_path();
     
    /* Tworzy pelna sciezke do pliku wylonywalnego na podstawie sciezki fuse */
    char* myfs_file_full_path(const char* fusepath);
     
    /* Sprawdza czy plik okreslony przez sciezke fuse rzeczywiscie istnieje w normalnym systemie plikow */
    int myfs_file_exists(const char* fusepath);
     
    /* Owtiera katalog do czytania */
    DIR* myfs_dir_open(char* dir_name);
     
    /* Pobiera kolejna pozycje z katalogu do czytania */
    struct dirent* myfs_dir_read(DIR* d);
     
    /* Zamyka otwarty katalog */
    int myfs_dir_close(DIR* d);
    /* --------------------------------------------------- */
     
    #endif
  2. Skompiluj program poleceniem
    gcc myfs-main.c myfs_tools.c `pkg-config fuse --cflags --libs` -o fsname

    zastąp fsname swoją wymyśloną nazwą systemu plików.

  3. W swoim katalogu domowym stwórz folder fuse a w nim plik id:
    echo '#!/bin/bash' > ~/fuse/id
    echo 'id -u' >> ~/fuse/id
  4. Ustaw odpowiednie prawa dostępu:
    chmod 755 ~/fuse/id
  5. W miejscu gdzie tworzony jest plik wykonywalny programu stwórz folder, który będzie punktem montowania nowego systemu plików.
  6. Zamontuj swój system plików.
  7. Sprawdź zawartość katalogu - powinien pojawić się plik id.
  8. Spróbuj wyświetlić plik używając np. programu cat.
  9. Przeanalizuj program w celu zrozumienia otrzymanego rezultatu:
    1. Przy pomocy ls -l wyświetl zawartość katalogu, skąd się wzięły takie a nie inne atrybuty pliku (prawa dostępu, typ pliku, rozmiar, właściciel, itp.).
    2. Dlaczego wyświetlenie pliku powoduje uruchomienie skryptu? Gdzie to jest zdefiniowane w programie?
Ćwiczenie 2 - Wyświetlanie zawartości pliku
  1. W swoim katalogu domowym stwórz drugi skrypt:
    echo '#!/bin/bash' > ~/fuse/procesy
    echo 'ps uax' >> ~/fuse/procesy
  2. Ustaw prawa do wykonywania.
  3. Zamontuj swój system plików i przy jego pomocy spróbuj wyświetlić zawartość powyższych plików i odpowiedzieć na pytania:
    • Czy pliki wyświetla się poprawnie?
    • Dlaczego otrzymano taki efekt?
  4. :!: Spróbuj naprawić powstały błąd.
Ćwiczenie 3 - Atrybuty plików
  1. Rozbuduj program tak aby wyświetlał tylko pliki regularne: Użyj pola entry→d_type.
  2. Rozbuduj program tak aby pliki wykonywalne miały usunięty atrybut zapisu w. Użyj pola st_mode.
  3. Rozbuduj program tak aby efektem odczytu plików które możemy uruchomić był odczyt rezultatów ich działania natomiast plików niewykonywalnych (z naszego punktu widzenia) wyświetlenie ich treści. Użyj funkcji access, pread.
Ćwiczenie 4 - Zapis i odczyt do/z plików
  1. W swoim katalogu domowych stwórz plik o dowolnej nazwie, który posiada prawo do zapisu i jednocześnie nie posiada prawa do uruchamiania.
  2. Przy pomocy echo spróbuj coś zapisać do tego pliku za pośrednictwem swojego systemu pliku:
    echo test > nazwa_pliku
  3. Dlaczego wypisywany jest komunikat o braku dostępu (przecież masz prawo do zapisu) :?: :!:
  4. Jak pozbyć się tego komunikatu i sprawić, żeby w przypadku próby zapisu system informował o niezaimplementowanej funkcji fuse np.:
    myfs/plik: Nie zaimplementowana funkcja
  5. Rozbuduj program o możliwość zapisu do pliku:
    1. Dopisz implementację funkcji myfs_write. Prototyp funkcji znajdziesz tutaj.
    2. Dopisz nazwę funkcji w strukturze definiującej listę zaimplementowanych operacji.
    3. Napisz implementację funkcji. Użyj pwrite.
  6. Spróbuj ponownie zapisać do pliku jakieś dane:
    1. Czy teraz jest to możliwe?
    2. Na czym polega problem?
    3. Zaimplementuj brakującą funkcję.
  7. Ponownie wykonaj zapis:
    1. Użyj zwykłego przekierowania stdout na plik
      echo test > nazwa_pliku
    2. Użyj dopisywania do pliku
      echo test2 >> nazwa_pliku
    3. Sprawdź czy otrzymane rezultaty są poprawne.
    4. Jeżeli nie poszukaj błędu: sprawdź funkcję myfs_write czy użyta w niej funkcja open używa flagi O_APPEND.
  8. Jeżeli wszystko działa sprawdź czy (możesz użyć jakiegoś edytora tekstowego np. emacs):
    1. możesz czytać pliki wykonywalne → powinno być to możliwe, a rezultatem powinno być wyjście z procesu.
    2. możesz czytać pliki niewykonywalne z odjętym prawem do czytania → nie powinno być to możliwe, próba odczytu powinna skutkować komunikatem o braku dostępu.
    3. możesz czytać pliki niewykonywalne prawem do czytania → powinno być to możliwe.
    4. możesz zapisywać do plików wykonywalnych → nie powinno być to możliwe, próba zapisu powinna skutkować komunikatem o braku dostępu.
    5. możesz zapisywać do plików niewykonywalnych, z ustawionym prawem do zapisu → powinno być to możliwe.
    6. możesz zapisywać do plików niewykonywalnych, z odjętym prawem do zapisu → nie powinno być to możliwe, próba zapisu powinna skutkować komunikatem o braku dostępu.
  9. Jeżeli w którymkolwiek punkcie zachowanie twojego systemu plików jest inne popraw jego działanie.
Ćwiczenie 5 - Prawa dostępu
  1. Spróbuj zmienić prawa dostępu do jednego z plików np. id.
  2. Znowu pojawia się komunikat o niezaimplementowanej funkcji. Dodaj brakującą funkcję chmod:
    1. Jej prototyp znajdziesz tutaj.
    2. Do implementacji funkcji użyj funkcji systemowej chmod.
  3. Dodaj zaimplementowaną funkcję do listy dozwolonych operacji w systemie plików.
  4. Sprawdź działanie praw dostępu:
    1. Wyświetl plik wykonywalny.
    2. Zmień prawa dostępu
      chmod a-x plik
    3. Wyświetl plik ponownie.
  5. Jaki jest rezultat?
  6. Zmień wybrany plik zapisując do niego inne polecenie do wykonania:
    1. Zmodyfikuj wybrany plik przy pomocy edytora (np. emacs, pico, vi) lub przekierowania strumienia.
    2. Wyświetl plik wykonywalny → powinieneś otrzymać treść skryptu.
    3. Zmień prawa dostępu
      chmod a+x plik
    4. Wyświetl plik ponownie → powinieneś otrzymać rezultat działania skryptu.
  7. Gratulacje! Teraz przy pomocy swojego systemu plików możesz edytować skrypty i je uruchamiać :!: :-)
  8. Stwórz odpowiednie pliki, które pobierają wybrane informacje o sprzęcie, systemie operacyjnym, etc.

Ciekawe systemy plików

Poniżej przestawiona została mała (aczkolwiek interesująca) część różnych systemów plików napisanych w oparciu o FUSE:

Pełna lista systemów plików jest umieszczona na stronie wiki projektu oraz w Wikipedii.

Źródła