Objectif du cours
Ce cours explique comment Linux représente les fichiers, les répertoires, les chemins, les liens physiques, les liens symboliques, et comment utiliser les principales fonctions système associées :
- link()
- unlink()
- symlink()
- readlink()
- stat() / lstat()
- rename()
- mkdir()
- rmdir()
- remove()
- opendir()
- readdir()
- closedir()
- rewinddir()
- dirfd()
- nftw()
- getcwd()
- chdir()
- fchdir()
- openat() et les fonctions *at()
- chroot()
- realpath()
- dirname() / basename()
Un nom de fichier est seulement une entrée dans un répertoire. Cette entrée pointe vers un inode. L’inode décrit réellement le fichier.
1. Le modèle mental fondamental
Sous Linux, il faut distinguer plusieurs notions :
- le chemin
- le nom du fichier
- l’entrée de répertoire
- l’inode
- les données du fichier
- le descripteur de fichier ouvert
Code: Select all
/home/user/demo.txt
Mais le fichier n’est pas vraiment cette chaîne.
Le répertoire /home/user contient une entrée qui ressemble conceptuellement à ceci :
Code: Select all
demo.txt -> inode 12345
- type du fichier ;
- permissions ;
- propriétaire ;
- groupe ;
- taille ;
- timestamps ;
- nombre de liens physiques ;
- emplacement des blocs de données.
Code: Select all
chemin -> répertoires traversés -> entrée de répertoire -> inode -> données
Code: Select all
nom = fichier
2. Qu’est-ce qu’un répertoire ?
Un répertoire est un fichier spécial.
Il ne contient pas du texte classique. Il contient une liste d’associations :
Code: Select all
nom -> numéro d’inode
Code: Select all
/home/user
notes.txt -> inode 1001
image.png -> inode 1002
projet -> inode 1003
lien_symbolique -> inode 1004
Code: Select all
open("/home/user/notes.txt", O_RDONLY);
Code: Select all
/
-> home
-> user
-> notes.txt
3. Chemin absolu et chemin relatif
Un chemin absolu commence par /.
Exemple :
Code: Select all
/home/user/demo.txt
/etc/passwd
/tmp/test.txt
Un chemin relatif ne commence pas par /.
Exemple :
Code: Select all
demo.txt
../test.txt
src/main.c
Exemple :
Code: Select all
répertoire courant = /home/user/projet
chemin relatif = src/main.c
chemin réel = /home/user/projet/src/main.c
Dans un répertoire, on rencontre souvent deux entrées spéciales :
Code: Select all
. -> le répertoire courant lui-même
.. -> le répertoire parent
Code: Select all
/home/user/projet
Code: Select all
. désigne /home/user/projet
.. désigne /home/user
Code: Select all
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
Un lien physique, ou hard link, est un autre nom pour le même inode.
Exemple shell :
Code: Select all
echo "hello" > a.txt
ln a.txt b.txt
Code: Select all
a.txt -> inode 500
b.txt -> inode 500
Si on modifie a.txt, on verra la modification via b.txt, parce que ce sont deux noms pour le même inode.
On peut vérifier avec :
Code: Select all
ls -li
Code: Select all
500 -rw-r--r-- 2 user user 6 a.txt
500 -rw-r--r-- 2 user user 6 b.txt
Le 2 indique le nombre de liens physiques vers cet inode.
À retenir :
- un lien physique ne contient pas un chemin ;
- il est simplement une autre entrée de répertoire vers le même inode ;
- il ne copie pas les données ;
- il ne crée pas un nouveau fichier indépendant ;
- il ne fonctionne généralement pas entre deux systèmes de fichiers différents ;
- les utilisateurs normaux ne peuvent généralement pas créer de hard links vers des répertoires.
Prototype :
Code: Select all
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
newpath est le nouveau nom à créer.
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (link("a.txt", "b.txt") == -1) {
perror("link");
return 1;
}
printf("Lien physique créé.\n");
return 0;
}
Code: Select all
a.txt -> même inode
b.txt -> même inode
Code: Select all
link("a.txt", "b.txt");
Cela ajoute seulement une nouvelle entrée de répertoire.
7. unlink() — supprimer un nom
Prototype :
Code: Select all
#include <unistd.h>
int unlink(const char *pathname);
Il ne faut pas penser :
Code: Select all
unlink() détruit toujours immédiatement le fichier
Code: Select all
unlink() retire un nom qui pointait vers un inode
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (unlink("a.txt") == -1) {
perror("unlink");
return 1;
}
printf("Nom supprimé.\n");
return 0;
}
Code: Select all
a.txt -> inode 500
b.txt -> inode 500
Code: Select all
unlink("a.txt");
Code: Select all
b.txt -> inode 500
L’inode et les blocs de données sont réellement libérés seulement quand :
- le nombre de liens physiques tombe à 0 ;
- aucun processus ne garde encore le fichier ouvert.
C’est un point très important.
Exemple :
Code: Select all
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd = open("temp.txt", O_CREAT | O_RDWR | O_TRUNC, 0600);
if (fd == -1) {
perror("open");
return 1;
}
write(fd, "hello\n", 6);
if (unlink("temp.txt") == -1) {
perror("unlink");
close(fd);
return 1;
}
/* Le nom temp.txt n'existe plus dans le répertoire. */
/* Mais le fichier existe encore tant que fd est ouvert. */
write(fd, "world\n", 6);
close(fd);
/* Maintenant, si aucun autre descripteur n'existe,
le fichier peut être réellement libéré. */
return 0;
}
Mais il reste accessible via fd.
C’est souvent utilisé pour créer des fichiers temporaires qui disparaissent automatiquement quand le processus termine ou ferme le descripteur.
9. Les liens symboliques
Un lien symbolique, ou symlink, est différent d’un lien physique.
Un lien symbolique est un fichier spécial qui contient un chemin.
Exemple shell :
Code: Select all
ln -s original.txt lien.txt
Code: Select all
lien.txt contient le texte "original.txt"
Code: Select all
lien.txt -> "original.txt"
original.txt -> inode 700
Comparaison :
Code: Select all
Lien physique :
a.txt -> inode 100
b.txt -> inode 100
Lien symbolique :
lien.txt contient "a.txt"
a.txt -> inode 100
Si la cible du lien symbolique disparaît, le lien symbolique peut devenir cassé.
10. symlink() — créer un lien symbolique
Prototype :
Code: Select all
#include <unistd.h>
int symlink(const char *target, const char *linkpath);
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (symlink("original.txt", "lien.txt") == -1) {
perror("symlink");
return 1;
}
printf("Lien symbolique créé.\n");
return 0;
}
Code: Select all
original.txt
La cible n’a pas besoin d’exister au moment de la création du lien symbolique.
Exemple :
Code: Select all
symlink("fichier_inexistant.txt", "lien.txt");
Mais le lien sera cassé tant que fichier_inexistant.txt n’existe pas.
11. readlink() — lire la cible d’un lien symbolique
Prototype :
Code: Select all
#include <unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
Elle ne lit pas les données de la cible.
Elle lit le chemin stocké dans le lien.
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char buffer[4096];
ssize_t n;
n = readlink("lien.txt", buffer, sizeof(buffer) - 1);
if (n == -1) {
perror("readlink");
return 1;
}
buffer[n] = '\0';
printf("Le lien pointe vers : %s\n", buffer);
return 0;
}
readlink() ne met pas automatiquement le caractère nul final \0.
Il faut le faire soi-même :
Code: Select all
buffer[n] = '\0';
Ces deux fonctions servent à obtenir des informations sur un fichier.
Mais leur comportement diffère avec les liens symboliques.
stat() suit le lien symbolique.
lstat() inspecte le lien symbolique lui-même.
Prototype :
Code: Select all
#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
Code: Select all
original.txt -> inode 100
lien.txt -> "original.txt"
Code: Select all
stat("lien.txt", &st);
Si on fait :
Code: Select all
lstat("lien.txt", &st);
Exemple de code :
Code: Select all
#include <stdio.h>
#include <sys/stat.h>
int main(void)
{
struct stat st;
if (lstat("lien.txt", &st) == -1) {
perror("lstat");
return 1;
}
if (S_ISLNK(st.st_mode)) {
printf("C'est un lien symbolique.\n");
}
return 0;
}
Code: Select all
stat() -> suit le lien
lstat() -> regarde le lien lui-même
Un lien symbolique cassé est un lien dont la cible n’existe plus.
Exemple :
Code: Select all
echo "hello" > original.txt
ln -s original.txt lien.txt
rm original.txt
Code: Select all
lien.txt -> "original.txt"
Le lien symbolique existe encore, mais il est cassé.
On appelle cela un dangling symlink.
14. rename() — renommer ou déplacer
Prototype :
Code: Select all
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
Exemple :
Code: Select all
rename("ancien.txt", "nouveau.txt");
Le noyau modifie surtout les entrées de répertoire.
Modèle mental :
Code: Select all
avant :
ancien.txt -> inode 300
après :
nouveau.txt -> inode 300
Les blocs de données ne bougent pas.
C’est pour cela que rename() est très rapide.
15. Atomicité de rename()
rename() est important parce que l’opération est généralement atomique.
Cela signifie qu’on n’a pas un état intermédiaire visible où le fichier serait “à moitié renommé”.
C’est très utile pour remplacer un fichier proprement.
Exemple classique :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
FILE *f = fopen("config.tmp", "w");
if (f == NULL) {
perror("fopen");
return 1;
}
fprintf(f, "option=42\n");
fclose(f);
if (rename("config.tmp", "config.txt") == -1) {
perror("rename");
return 1;
}
return 0;
}
- on écrit dans un fichier temporaire ;
- quand l’écriture est terminée, on remplace l’ancien fichier par rename() ;
- les autres programmes voient soit l’ancien fichier, soit le nouveau, mais pas un fichier à moitié écrit.
Prototype :
Code: Select all
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
Code: Select all
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
int main(void)
{
if (mkdir("demo", 0755) == -1) {
perror("mkdir");
return 1;
}
printf("Répertoire créé.\n");
return 0;
}
Exemple :
Code: Select all
0755
Code: Select all
propriétaire : lecture + écriture + exécution
groupe : lecture + exécution
autres : lecture + exécution
Piège : umask
Les permissions finales sont influencées par umask.
Si tu demandes :
Code: Select all
mkdir("demo", 0777);
Code: Select all
0022
Code: Select all
0755
Prototype :
Code: Select all
#include <unistd.h>
int rmdir(const char *pathname);
Exemple :
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (rmdir("demo") == -1) {
perror("rmdir");
return 1;
}
printf("Répertoire supprimé.\n");
return 0;
}
Pour supprimer un arbre complet, il faut :
- ouvrir le répertoire ;
- parcourir ses entrées ;
- supprimer les fichiers avec unlink() ;
- supprimer récursivement les sous-répertoires ;
- terminer par rmdir() sur le dossier parent.
Prototype :
Code: Select all
#include <stdio.h>
int remove(const char *pathname);
Elle peut supprimer :
- un fichier ;
- un répertoire vide.
Code: Select all
si pathname est un fichier -> comportement proche de unlink()
si pathname est un répertoire -> comportement proche de rmdir()
Code: Select all
#include <stdio.h>
int main(void)
{
if (remove("test.txt") == -1) {
perror("remove");
return 1;
}
return 0;
}
Code: Select all
unlink() -> supprime un nom de fichier
rmdir() -> supprime un répertoire vide
remove() -> supprime un fichier ou un répertoire vide
remove() ne supprime pas récursivement un répertoire non vide.
19. Lire un répertoire avec opendir(), readdir(), closedir()
Pour parcourir un répertoire en C, on utilise :
Code: Select all
#include <dirent.h>
DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
Code: Select all
#include <stdio.h>
#include <dirent.h>
int main(void)
{
DIR *dirp;
struct dirent *entry;
dirp = opendir(".");
if (dirp == NULL) {
perror("opendir");
return 1;
}
while ((entry = readdir(dirp)) != NULL) {
printf("%s\n", entry->d_name);
}
closedir(dirp);
return 0;
}
readdir() lit les entrées une par une.
closedir() ferme le flux.
20. struct dirent
readdir() retourne un pointeur vers une structure struct dirent.
Elle contient notamment :
Code: Select all
ino_t d_ino;
char d_name[];
Code: Select all
d_name
Exemple :
Code: Select all
.
..
main.c
demo.txt
include
readdir() ne donne pas toutes les informations du fichier.
Pour obtenir la taille, les permissions, le type exact, etc., il faut souvent appeler :
Code: Select all
stat()
lstat()
fstatat()
Prototype :
Code: Select all
#include <dirent.h>
void rewinddir(DIR *dirp);
Exemple :
Code: Select all
#include <stdio.h>
#include <dirent.h>
int main(void)
{
DIR *dirp;
struct dirent *entry;
dirp = opendir(".");
if (dirp == NULL) {
perror("opendir");
return 1;
}
printf("Premier parcours :\n");
while ((entry = readdir(dirp)) != NULL) {
printf("%s\n", entry->d_name);
}
rewinddir(dirp);
printf("Deuxième parcours :\n");
while ((entry = readdir(dirp)) != NULL) {
printf("%s\n", entry->d_name);
}
closedir(dirp);
return 0;
}
22. dirfd() — récupérer le descripteur associé à un DIR *
Prototype :
Code: Select all
#include <dirent.h>
int dirfd(DIR *dirp);
Exemple :
Code: Select all
#include <stdio.h>
#include <dirent.h>
#include <unistd.h>
int main(void)
{
DIR *dirp = opendir(".");
if (dirp == NULL) {
perror("opendir");
return 1;
}
int fd = dirfd(dirp);
if (fd == -1) {
perror("dirfd");
closedir(dirp);
return 1;
}
printf("Descripteur du répertoire : %d\n", fd);
closedir(dirp);
return 0;
}
- openat()
- fstatat()
- unlinkat()
- mkdirat()
- renameat()
Un parcours récursif manuel ressemble à ceci :
Code: Select all
opendir()
readdir()
ignorer . et ..
lstat()
si fichier -> traiter
si répertoire -> appeler récursivement la fonction
closedir()
Code: Select all
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <limits.h>
static void walk(const char *path)
{
DIR *dirp;
struct dirent *entry;
dirp = opendir(path);
if (dirp == NULL) {
perror(path);
return;
}
while ((entry = readdir(dirp)) != NULL) {
char fullpath[PATH_MAX];
struct stat st;
if (strcmp(entry->d_name, ".") == 0 ||
strcmp(entry->d_name, "..") == 0) {
continue;
}
snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name);
if (lstat(fullpath, &st) == -1) {
perror(fullpath);
continue;
}
printf("%s\n", fullpath);
if (S_ISDIR(st.st_mode)) {
walk(fullpath);
}
}
closedir(dirp);
}
int main(void)
{
walk(".");
return 0;
}
Parce qu’on ne veut pas suivre aveuglément les liens symboliques.
Un lien symbolique peut créer une boucle ou pointer vers un endroit inattendu.
24. nftw() — parcourir une arborescence
nftw() signifie souvent new file tree walk.
Cette fonction parcourt une arborescence de fichiers et appelle une fonction callback pour chaque élément rencontré.
Prototype simplifié :
Code: Select all
#define _XOPEN_SOURCE 500
#include <ftw.h>
int nftw(const char *dirpath,
int (*func)(const char *pathname,
const struct stat *statbuf,
int typeflag,
struct FTW *ftwbuf),
int nopenfd,
int flags);
Code: Select all
opendir()
readdir()
lstat()
récursion
closedir()
Exemple minimal :
Code: Select all
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <ftw.h>
static int callback(const char *pathname,
const struct stat *statbuf,
int typeflag,
struct FTW *ftwbuf)
{
(void)statbuf;
(void)ftwbuf;
printf("%s\n", pathname);
return 0;
}
int main(void)
{
if (nftw(".", callback, 10, FTW_PHYS) == -1) {
perror("nftw");
return 1;
}
return 0;
}
flags permet de modifier le comportement du parcours.
25. typeflag dans nftw()
La callback reçoit un paramètre typeflag.
Il indique le type d’objet rencontré.
Valeurs importantes :
Code: Select all
FTW_F
Fichier normal.
FTW_D
Répertoire visité avant ses enfants.
FTW_DNR
Répertoire non lisible.
FTW_DP
Répertoire visité après ses enfants.
FTW_NS
stat() impossible sur cet élément.
FTW_SL
Lien symbolique.
FTW_SLN
Lien symbolique cassé.
Code: Select all
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <ftw.h>
static int callback(const char *pathname,
const struct stat *statbuf,
int typeflag,
struct FTW *ftwbuf)
{
(void)statbuf;
(void)ftwbuf;
switch (typeflag) {
case FTW_F:
printf("Fichier : %s\n", pathname);
break;
case FTW_D:
printf("Dossier : %s\n", pathname);
break;
case FTW_SL:
printf("Lien symbolique : %s\n", pathname);
break;
case FTW_SLN:
printf("Lien symbolique cassé : %s\n", pathname);
break;
default:
printf("Autre : %s\n", pathname);
break;
}
return 0;
}
int main(void)
{
nftw(".", callback, 10, FTW_PHYS);
return 0;
}
FTW_PHYS
Ne suit pas les liens symboliques.
C’est souvent le choix le plus sûr.
Code: Select all
nftw(".", callback, 10, FTW_PHYS);
Visite les enfants avant le répertoire parent.
Très utile pour supprimer récursivement :
Code: Select all
fichiers d'abord
sous-répertoires ensuite
répertoire parent à la fin
Ne traverse pas vers un autre système de fichiers.
FTW_CHDIR
Change le répertoire courant pendant le parcours.
À utiliser avec prudence, surtout dans un programme multi-thread.
27. Exemple avec nftw() : calculer la taille totale
Code: Select all
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <ftw.h>
#include <sys/stat.h>
static long long total_size = 0;
static int callback(const char *pathname,
const struct stat *statbuf,
int typeflag,
struct FTW *ftwbuf)
{
(void)pathname;
(void)ftwbuf;
if (typeflag == FTW_F) {
total_size += statbuf->st_size;
}
return 0;
}
int main(int argc, char *argv[])
{
const char *path = ".";
if (argc > 1) {
path = argv[1];
}
if (nftw(path, callback, 10, FTW_PHYS) == -1) {
perror("nftw");
return 1;
}
printf("Taille totale : %lld octets\n", total_size);
return 0;
}
Chaque processus possède un current working directory, c’est-à-dire un répertoire courant.
Les chemins relatifs sont interprétés par rapport à ce répertoire.
Exemple :
Code: Select all
répertoire courant = /home/user/projet
open("main.c", O_RDONLY)
Code: Select all
/home/user/projet/main.c
Prototype :
Code: Select all
#include <unistd.h>
char *getcwd(char *buf, size_t size);
Code: Select all
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
int main(void)
{
char buffer[PATH_MAX];
if (getcwd(buffer, sizeof(buffer)) == NULL) {
perror("getcwd");
return 1;
}
printf("Répertoire courant : %s\n", buffer);
return 0;
}
30. chdir() — changer le répertoire courant
Prototype :
Code: Select all
#include <unistd.h>
int chdir(const char *path);
Code: Select all
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (chdir("/tmp") == -1) {
perror("chdir");
return 1;
}
printf("Répertoire courant changé.\n");
return 0;
}
Code: Select all
chdir("/tmp");
Code: Select all
open("test.txt", O_RDONLY);
Code: Select all
/tmp/test.txt
Le répertoire courant appartient au processus.
Dans un programme multi-thread, changer le répertoire courant peut perturber les autres threads.
31. fchdir() — changer de répertoire avec un descripteur
Prototype :
Code: Select all
#include <unistd.h>
int fchdir(int fd);
Exemple :
Code: Select all
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int olddir;
olddir = open(".", O_RDONLY | O_DIRECTORY);
if (olddir == -1) {
perror("open");
return 1;
}
if (chdir("/tmp") == -1) {
perror("chdir");
close(olddir);
return 1;
}
printf("On est dans /tmp.\n");
if (fchdir(olddir) == -1) {
perror("fchdir");
close(olddir);
return 1;
}
printf("On est revenu dans l'ancien répertoire.\n");
close(olddir);
return 0;
}
32. Les fonctions *at()
Linux propose des fonctions qui travaillent relativement à un descripteur de répertoire :
Code: Select all
openat()
fstatat()
unlinkat()
renameat()
mkdirat()
symlinkat()
readlinkat()
Code: Select all
chemin relatif par rapport au répertoire courant global
Code: Select all
chemin relatif par rapport à un répertoire déjà ouvert
Prototype simplifié :
Code: Select all
#include <fcntl.h>
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
Code: Select all
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int dfd;
int fd;
dfd = open("/tmp", O_RDONLY | O_DIRECTORY);
if (dfd == -1) {
perror("open directory");
return 1;
}
fd = openat(dfd, "test.txt", O_RDONLY);
if (fd == -1) {
perror("openat");
close(dfd);
return 1;
}
printf("/tmp/test.txt ouvert.\n");
close(fd);
close(dfd);
return 0;
}
Code: Select all
openat(dfd, "test.txt", O_RDONLY)
Code: Select all
ouvre test.txt relativement au répertoire représenté par dfd
Code: Select all
/tmp/test.txt
Les fonctions *at() sont importantes pour :
- éviter de dépendre du répertoire courant global ;
- écrire du code plus robuste ;
- réduire certains risques de race conditions ;
- travailler proprement dans des programmes multi-thread ;
- garder une référence stable vers un répertoire déjà ouvert.
Code: Select all
1. Je vérifie un chemin.
2. Quelqu'un modifie le système de fichiers.
3. J'utilise le chemin.
C’est le problème TOCTOU :
Code: Select all
Time Of Check To Time Of Use
35. AT_FDCWD
Certaines fonctions *at() acceptent la constante :
Code: Select all
AT_FDCWD
Code: Select all
utilise le répertoire courant comme base
Code: Select all
openat(AT_FDCWD, "test.txt", O_RDONLY);
Code: Select all
open("test.txt", O_RDONLY);
Prototype :
Code: Select all
#include <unistd.h>
int chroot(const char *path);
Normalement, / désigne la racine du système.
Après :
Code: Select all
chroot("/srv/jail");
Code: Select all
/ comme /srv/jail
Code: Select all
/etc/passwd
Code: Select all
/srv/jail/etc/passwd
37. chroot() n’est pas une sécurité parfaite
chroot() seul ne suffit pas pour faire une sandbox moderne fiable.
Problèmes possibles :
- le processus peut garder des privilèges élevés ;
- il peut garder des descripteurs ouverts vers l’extérieur ;
- l’environnement chroot peut être mal préparé ;
- des erreurs de configuration peuvent permettre une fuite.
Code: Select all
int fd = open("/", O_RDONLY | O_DIRECTORY);
chroot("/srv/jail");
fchdir(fd);
Une isolation moderne utilise plutôt une combinaison de mécanismes :
- namespaces ;
- capabilities ;
- seccomp ;
- AppArmor / SELinux ;
- containers ;
- pivot_root ;
- réduction des privilèges.
Prototype :
Code: Select all
#include <stdlib.h>
char *realpath(const char *pathname, char *resolved_path);
Elle résout :
- les chemins relatifs ;
- . ;
- .. ;
- les liens symboliques.
Code: Select all
./demo/../test/lien
Code: Select all
/home/user/test/original.txt
Exemple de code :
Code: Select all
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void)
{
char buffer[PATH_MAX];
if (realpath(".", buffer) == NULL) {
perror("realpath");
return 1;
}
printf("Chemin canonique : %s\n", buffer);
return 0;
}
realpath() est utile pour afficher ou normaliser un chemin.
Mais en sécurité, il faut rester prudent.
Exemple de problème :
Code: Select all
1. realpath() vérifie un chemin.
2. Un autre processus modifie le chemin.
3. Le programme ouvre ensuite le chemin.
C’est encore un problème de type TOCTOU.
Donc realpath() ne remplace pas une conception sécurisée avec des descripteurs ouverts et des fonctions *at().
40. dirname() et basename()
Ces fonctions découpent un chemin.
Prototypes :
Code: Select all
#include <libgen.h>
char *dirname(char *path);
char *basename(char *path);
basename() renvoie le dernier composant.
Exemple :
Code: Select all
/home/user/test.txt
Code: Select all
dirname -> /home/user
basename -> test.txt
Code: Select all
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <libgen.h>
int main(void)
{
const char *path = "/home/user/test.txt";
char *copy1 = strdup(path);
char *copy2 = strdup(path);
if (copy1 == NULL || copy2 == NULL) {
perror("strdup");
free(copy1);
free(copy2);
return 1;
}
printf("dirname : %s\n", dirname(copy1));
printf("basename : %s\n", basename(copy2));
free(copy1);
free(copy2);
return 0;
}
Ces fonctions peuvent modifier la chaîne passée en argument.
Donc il ne faut pas faire :
Code: Select all
dirname("/home/user/test.txt");
Il faut utiliser une chaîne modifiable :
Code: Select all
char path[] = "/home/user/test.txt";
dirname(path);
Cas particuliers :
Code: Select all
chemin dirname basename
/home/user/test.txt /home/user test.txt
/home/user/ /home user
test.txt . test.txt
/ / /
"" . .
Code: Select all
Fonction Rôle principal Piège principal
link() Créer un lien physique Ne copie pas le fichier
unlink() Supprimer un nom Ne détruit pas forcément les données immédiatement
symlink() Créer un lien symbolique La cible peut ne pas exister
readlink() Lire la cible d'un symlink N'ajoute pas automatiquement \0
stat() Infos sur la cible Suit les liens symboliques
lstat() Infos sur le lien lui-même Ne suit pas le symlink
rename() Renommer/déplacer Très rapide sur le même FS car pas de recopie
mkdir() Créer un répertoire Le mode est filtré par umask
rmdir() Supprimer un répertoire vide Échoue si non vide
remove() Supprimer fichier ou dossier vide Pas récursif
opendir() Ouvrir un flux de répertoire À fermer avec closedir()
readdir() Lire les entrées Ne donne pas toutes les métadonnées
closedir() Fermer un flux de répertoire Libère les ressources
rewinddir() Revenir au début du flux Relit depuis le début
dirfd() Obtenir le fd d'un DIR * Utile avec les fonctions *at()
nftw() Parcourir une arborescence Attention aux symlinks
getcwd() Lire le répertoire courant Peut échouer si buffer trop petit
chdir() Changer le répertoire courant Impact global sur le processus
fchdir() Changer via un fd Utile pour restaurer un ancien dossier
openat() Ouvrir relativement à un dirfd Plus robuste que chdir()+open()
chroot() Changer la racine visible Pas une sécurité parfaite seul
realpath() Résoudre un chemin canonique Peut créer un TOCTOU si mal utilisé
dirname() Obtenir le dossier parent Peut modifier la chaîne
basename() Obtenir le dernier composant Peut modifier la chaîne
- Croire que le nom du fichier est le fichier lui-même.
- Croire qu’un hard link est un raccourci.
- Croire qu’un symlink pointe directement vers l’inode.
- Croire que unlink() détruit toujours les données immédiatement.
- Oublier qu’un fichier supprimé peut rester vivant s’il est encore ouvert.
- Confondre stat() et lstat().
- Utiliser readlink() sans ajouter \0.
- Penser que remove() supprime récursivement un dossier.
- Oublier que rmdir() ne supprime que les répertoires vides.
- Utiliser chdir() sans réfléchir dans un programme multi-thread.
- Croire que chroot() est une vraie sandbox complète.
- Utiliser realpath() comme solution magique de sécurité.
Projet 1 : ls simplifié
- Utiliser opendir().
- Lire les entrées avec readdir().
- Afficher d_name.
- Ignorer ou non . et ...
- Parcourir un dossier.
- Appeler lstat() sur chaque entrée.
- Tester S_ISLNK(st.st_mode).
- Lire la cible avec readlink().
- Trouver les liens symboliques avec lstat().
- Tester si stat() échoue sur la cible.
- Afficher les liens cassés.
- Utiliser opendir() / readdir().
- Faire une récursion sur les sous-répertoires.
- Afficher l’indentation selon la profondeur.
- Utiliser nftw().
- Additionner st_size pour les fichiers normaux.
- Ne pas suivre les symlinks avec FTW_PHYS.
- Parcourir avec nftw() et FTW_DEPTH.
- Supprimer les fichiers avec unlink().
- Supprimer les dossiers avec rmdir().
- Ajouter une option “dry-run” avant de supprimer réellement.
Le modèle final à retenir est :
Code: Select all
chemin
-> résolution dans les répertoires
-> entrée de répertoire
-> inode
-> métadonnées
-> blocs de données
Code: Select all
a.txt -> inode 100
b.txt -> inode 100
Code: Select all
lien.txt -> "a.txt"
a.txt -> inode 100
Code: Select all
a.txt -> inode 100
b.txt -> inode 100
unlink("a.txt")
b.txt -> inode 100
Code: Select all
nombre de liens physiques == 0
et
aucun descripteur ouvert ne le référence
Ce chapitre est essentiel pour comprendre la logique du système de fichiers Linux.
Il faut surtout retenir que Linux ne pense pas seulement en “noms de fichiers”.
Il pense en :
- chemins ;
- répertoires ;
- entrées de répertoire ;
- inodes ;
- liens physiques ;
- liens symboliques ;
- descripteurs ouverts ;
- racine visible du processus ;
- répertoire courant du processus.
- pourquoi rename() est rapide ;
- pourquoi unlink() ne détruit pas toujours immédiatement les données ;
- pourquoi un fichier supprimé peut rester ouvert ;
- pourquoi stat() et lstat() ne donnent pas la même chose ;
- pourquoi un symlink peut être cassé ;
- pourquoi les fonctions *at() existent ;
- pourquoi chroot() ne suffit pas à faire une sécurité complète.
Code: Select all
Un nom n'est pas le fichier.
Un chemin n'est pas le fichier.
Un répertoire associe des noms à des inodes.
L'inode représente réellement le fichier.
