New article on whiteout files
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline was successful

This commit is contained in:
nemunaire 2023-11-09 16:37:04 +01:00
parent 4e5d7b2a4c
commit 075b635446
4 changed files with 580 additions and 0 deletions

View File

@ -0,0 +1,256 @@
---
title: "Unveiling Whiteout Files: Do you know how file deletions are handled between layers of a Docker image?"
date: !!timestamp '2023-11-09 15:35:00'
image: /post/unveiling-whiteout-files/og.webp
tags:
- kernel
- container
- docker
- linux
---
Union file systems are a mechanism for merging two or more file systems, to present them unified, under a single mount point for the user.
The main idea behind this mechanism is to be able to alter the contents of the first file system (e.g. the contents of a CD-ROM) by writing all changes (additions, deletions, modifications) to the second (which could be a disk partition, a USB stick, ...).
While adding and modifying may seem trivial, deleting is not.
So let's explore in this article what *whiteout files* are and how they can simulate the deletion of a file.
<!-- more -->
Another common use of filesystem unions is in containers: container images are made up of layers.
If you launch a `php` container and then a `nginx` container, both images based on `debian`, you will only download the underlying `debian` image once.
Files from the `debian` image may be modified or deleted by an image such as `php` or `nginx`.
Thanks to union file system!
## Understanding Union File System
Unions file system share a number of concepts, which we will illustrate with the following diagram:
![File access by layer](overlayfs.png)
Here we see a two-layer file system, referred to in the jargon as two *branches*.
They are denoted *Lower* for the lowest layer and *upper* for the layer that is inserted on top of the *lower* layer; and finally *Merged* for the resulting view.
Some implementations support more than 2 branches, with sometimes complex access and modification policies.
When a file is deleted from the union, a so-called *whiteout file* is placed in the *upper* layer to indicate that this file should no longer be displayed in the *merged* layer.
The same concept applies to folders, which are referred to as *opaque directory*.
When accessing a file in the *lower* branch that has not been modified in *upper*, the *lower* file is accessed directly.
When a file is modified, its entire contents are copied from the *lower* branch to the *upper* branch.
A file that is added, overwritten or modified will therefore have its entire contents in the *upper* layer.
## History
The concept of *whiteout file* has its origins in the early development of file system unions.
[**Translucent File System**](http://mcvoy.com/lm/papers/SunOS.tfs.pdf) is undoubtedly the first implementation of the *whiteout file* concept.
Developed by David Hendricks in the 1980s for SunOS 3, the idea was to allow users of a machine to take advantage of the base system, making modifications without impacting other users, and without having access to other users' files.
The first *union mounts* were implemented with BSD 4.4, in the 90s.
The best-known implementation today is *UnionFS*, by Erez Zadok.
It was to be the implementation used for the Linux kernel, but like *aufs*, their code and solution didn't convince to be fully integrated.
It wasn't until 2014 that a *union mount* was integrated into the Linux kernel.
This is OverlayFS.
It arrived in kernel 3.18, after more than 4 years of rewrites and structural improvements, to reach the demanding and uncompromising level required for its integration into the official kernel.
{{% card color="info" title="What issues complicate the implementation of an union file system?" %}}
One of the trickiest problems is finding a way to represent file and folder deletions: it has to be a valid file (with or without metadata), as the information needs to be stored in a concrete way.
In many implementations, a `.wh.<filename>` file serves as a *whiteout file*, which can create conflicts with the user's own file names (or reduce the user's choice of file names).
A similar problem applies to folders: should you delete every file contained in the folder, or does the mere presence of an *opaque directory* prevents discovery?
Memory usage can quickly get out of hand, especially if the implementation allows a lot of branches, because if you want the system to perform well you'll need to have the topologies of each file system in memory.
Implementing `mmap(2)` is necessarily a nightmare: when a file is modified by two processes that `mmap(2)`, we normally expect to see the modifications in both processes, but the first to make a modification creates a new file in the writeable branch.
This makes it difficult to reconcile the pointers of the two processes.
Similarly, think about *hard links* management: all pointers to updated content should be modified in the write layer, but there is no pointer index, so it's not easy to find the files to be updated.
And let's not forget that the underlying file systems of each branch don't necessarily have the same constraints (file name sizes, extended attributes, metadata, accent encoding, etc.), so you have to juggle between them, while returning consistent errors where appropriate.
And many more besides.
Not least `readdir(2)`, which needs to be stable despite the turbulence that can occur between two calls, ...
See this series of articles summarizing the different implementations, their choices and differences:
<https://lwn.net/Articles/325369/>, <https://lwn.net/Articles/327738/>.
{{% /card %}}
In what follows, we'll be concentrating mainly on the operation of this file system, trying as far as possible to draw parallels with the others.
## Whiteout Files in Practice
First of all, you need to know how to set up such a file system.
Here's a general example of how to create a simple union between a read-only and a read/write file system:
```
mount -t overlay -olowerdir=/lower,upperdir=/upper,workdir=/work ignored /merged
```
The type to use is `overlay`, with the `lowerdir` options indicating the location of the folder(s) to be combined in read-only mode (separated by `:` when there are several), the directory containing the read/write system in the `upperdir` option, and don't forget the `workdir` option, a path on the same partition as the `upperdir`, which must be empty.
We end the call by giving the source device, which is useless in our case (`ignored` or any other string will do), and finally the folder to which our union will be mounted: `/merged` in the example.
### Usage in Containerization
Let's analyze a running Docker container to learn more.
First, we check that we're using the `overlay2` *storage driver*:
```
42sh$ docker info | grep "Storage Driver"
Storage Driver: overlay2
```
This is the case (depending on your kernel configuration, Docker may have chosen a different *driver*), so let's start the analysis:
```
42sh$ docker container run --rm -it debian
incntr$ mount | grep "on / "
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B62UNV3UB3X4TBWQMM6XCMM6W5:/var/lib/docker/overlay2/l/V6HGFN3C3PEW6CZ6XWRSHHDKJH,upperdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/diff,workdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/work)
```
Note that 2 `lowerdir` are used. These are symbolic links pointing to the folders identifying the layers (the names of the links are random, the aim being to have a shortened path to the layer's file system, as the number of characters that can be passed to the `mount(2)` system call is limited).
The lowest branch (furthest to the right of the `lowerdir` parameter) contains the single layer of our `debian` image, while the branch furthest to the left overlays a number of configuration files required to run the container (`/etc/hosts`, `resolv.conf`, ...).
The read/write branch is also registered in the `/var/lib/docker/overlay2` folder, and its identifier can be seen. The `upperdir` is in the `diff` folder, while the `workdir` is in the `work` folder, under the same layer ID.
We can also see the folders used by inspecting our:
```
42sh$ docker container inspect youthful_wilbur | jq .[0].GraphDriver.Data
```
```json
{
"LowerDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31-init/diff:/var/lib/docker/overlay2/2cc3656c06...c0fb91d6/diff",
"MergedDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/merged",
"UpperDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/diff",
"WorkDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/work"
}
```
If you test with an image with more layers, you'll get more `lowerdir`, one per layer. Feel free to run the same series of commands with the `python` image, for example.
### Adding files
At this point, if we look at the contents of our `upperdir` folder, we can see that it's empty. This is normal, since we haven't made any changes.
In our previously launched container, let's make a modification, by adding a:
```
incntr$ echo "newfile" > /root/foobar
```
```
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
└── root
└── foobar
```
Our new file, which is not the only one in the tree structure shown in the container, has been added, as you'd expect, to the read/write branch.
### Modifying files
If we make a change to a file, for example by adding a line, it's not just the difference that is stored in the write branch, but the whole file, as it has been modified:
```
incntr$ echo "Bienvenue dans le conteneur" >> /etc/issue
```
```
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
└── etc
└── issue
```
```
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/issue
Debian GNU/Linux 11 \n \l
Bienvenue dans le conteneur
```
### Deleting files
When you want to delete a file you've just added, there's not much you can do, since deleting the file from the write branch will make the file disappear from the mounted tree.
When it comes to deleting a file from a read-only branch, you need to be able to hide the file using a marker.
Depending on the *storage driver*, this marker is different: in `OverlayFS`, a deletion is materialized by a special character file of the same name.
```
incntr$ rm /etc/adduser.conf
```
```
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
/var/lib/docker/overlay2/1531651afa872006a4b2b9b913d5d8ee317cf12be7883517ba77f3d094f871b4/diff
└── etc
└── adduser.conf
```
```
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
cat: No such device or address
42sh$ stat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
File: /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: fe0bh/65035d Inode: 515773 Links: 2 Device type: 0,0
```
Note here `Device type: 0,0`.
To create a similar file ourselves, we would need to use:
```
42sh$ mkdir /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin
42sh$ mknod /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin/sh c 0 0
```
{{% card color="danger" title="Caution, undefined behavior!" %}}
Running this `mknod` command while the file system union is mounted elsewhere will not make the `/bin/sh` file disappear, as any modifications modifications that could be made to the branches outside the mounted system lead to explicitly undefined results.
{{% /card %}}
### Deletion on `unionfs` and AuFS
The concept of *whiteout file*, as we have seen, differs depending on the file system. It turns out that, although OverlayFS was integrated into the Linux kernel after many ups and downs, when specifying the format of the archives used to distribute layers, Docker now uses the AuFS format to represent deletions.
It is therefore important to know it too.
Instead of using a special file, AuFS creates a standard file `.wh.<filename>`, where `<filename>` is the name of the file to be hidden.
In order to adapt to the *storage driver*, when the archive is decompressed, Docker converts[^MOBYWHITEOUT] the *whiteout files* it encounters into the expected expected format.
[^MOBYWHITEOUT]: See the source code
<https://github.com/moby/moby/blob/master/pkg/archive/archive_linux.go#L27>
## Conclusion
Just when you thought you didn't want to know what *whiteout files* were all about, I'm sure that reading this article has given you a glimpse into the complexity of both *union mounts* and software that takes advantages of different implementations.
Now you know why, in particular, it's pointless to delete a large file in a layer other than the one that contributed it, for example:
```dockerfile
RUN wget https://dumps.wikimedia.org/enwiki/enwiki-pages-articles-multistream.xml.bz2
RUN ... # some other stuff
RUN rm enwiki-pages-articles-multistream.xml.bz2
```
Each `RUN` creates a separate layer, so our `enwiki-pages-articles-multistream.xml.bz2` file will be distributed with the first layer of our image, then a *whiteout file* will be inserted in the layer corresponding to the third `RUN`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1,324 @@
---
title: "Explorons les Whiteout files: tout savoir sur la suppression de fichiers dans nos conteneurs Docker"
date: !!timestamp '2023-11-09 15:35:00'
image: /post/unveiling-whiteout-files/og.webp
tags:
- kernel
- container
- docker
- linux
---
Les unions de systèmes de fichiers sont un mécanisme permettant de fusionner deux ou plusieurs systèmes de fichiers, pour les présenter unifiés, sous un seul point d'accès à l'utilisateur.
L'idée principale derrière ce mécanisme est de pouvoir permettre d'altérer le contenu du premier système de fichiers (par exemple le contenu d'un CD-ROM) en inscrivant toutes les modifications (ajouts, suppressions, modifications) dans le second (qui pourra être une partition d'un disque).
Si l'ajout et la modification peuvent sembler triviales, ce n'est pas le cas de la suppression. Alors explorons dans cet article ce que sont les *whiteout files* et comment ils permettent de simuler la suppression d'un fichier.
<!-- more -->
Un autre usage courant des unions de systèmes de fichiers se retrouve dans les conteneurs : les images des conteneurs sont composées de couches. Si vous lancez un conteneur `php` puis un conteneur `nginx`, deux images basées sur `debian`, vous ne téléchargerez qu'une seule fois l'image `debian` sous-jacente.
Des fichiers issus de l'image `debian` peuvent se trouver modifié ou supprimé par une image telle que `php` ou `nginx`.
Grâce à une union de système de fichiers!
## Comprendre les unions de systèmes de fichiers
Les unions de systèmes de fichiers partagent un certain nombre de concepts que
nous allons illustrer au travers du schéma suivant :
![Accès aux fichiers en fonction des couches](overlayfs.png)
On voit ici un système de fichiers à deux couches, on parle de deux *branches* dans
le jargon. Elles sont notées *Lower* pour la couche la plus basse et *upper* la
couche qui s'insère par dessus la couche *lower* ; et enfin *Merged* le
résultat. Certaines implémentations supportent plus que 2 branches, avec des
politiques d'accès et de modifications parfois complexes.
Lorsque l'on supprime un fichier de l'union, un fichier dit *whiteout file* est
placé dans la couche en écriture (*upper*) pour indiquer que ce fichier ne doit plus être
affiché dans la couche *merged*. Le même concept existe pour les dossiers,
on parle alors d'*opaque directory*.
Lorsqu'il s'agit d'accéder à un fichier présent dans la branche *lower* et qui
n'a pas été modifié dans *upper*, on accède directement au fichier de *lower*.
Lorsqu'un fichier est modifié, on recopie son contenu intégralement dans la
branche *upper*, depuis la branche *lower*. Un fichier qui est ajouté, écrasé
ou modifié aura donc son contenu intégralement dans la couche *upper*.
## Historique
Le concept de *whiteout file* trouve son origine dans les premiers développements des unions de systèmes de fichiers.
[**Translucent File System**](http://mcvoy.com/lm/papers/SunOS.tfs.pdf) est sans doute la première mise en œuvre du concept de *whiteout file*.
Développé par David Hendricks dans les années 1980 pour SunOS 3, il s'agissait de permettre aux utilisateurs d'une machine de profiter du système de base, en y apportant des modifications sans impacter les autres utilisateurs, et sans avoir accès aux fichiers des autres utilisateurs.
Vint ensuite les premières implémentation d'*union mounts* avec BSD 4.4, dans les années 90.
L'implémentation la plus connue aujourd'hui est l'*UnionFS*, de Erez Zadok.
Elle devait être l'implémentation utilisée pour le noyau Linux, mais tout comme *aufs*, leur code et leur solution ne convainct pas pour être pleinement intégré.
Il faudra attendre 2014, pour qu'un *union mount* soit intégré dans le noyau Linux.
Il s'agit d'OverlayFS.
Il est arrivé dans le noyau 3.18, après de plus de 4 années de réécritures et d'améliorations structurelles, pour atteindre le niveau d'exigence et sans compromis nécessaire à son intégration dans le noyau officiel.
{{% card color="info" title="Quelles problématiques rendent l'implémentation d'une union de systèmes de fichier compliquée ?" %}}
L'un des problèmes les plus délicats est de trouver une manière de représenter
les suppressions de fichiers et de dossiers : cela doit être un fichier valide
(avec ou sans métadonnée) car il faut pouvoir stocker l'information
concrètement. Dans de nombreuses implémentations, un fichier `.wh.<filename>`
sert de *whiteout file*, ce qui peut créer des conflits avec les noms des
fichiers de l'utilisateur (ou réduire ses choix de noms de fichiers).
Un problème similaire s'applique aux dossiers : est-ce qu'il faut supprimer
chaque fichier contenu dans le dossier ou la simple présence d'un *opaque
directory* empêche toute découverte ?
L'usage de la mémoire peut vite devenir incontrôlable, surtout si
l'implémentation autorise beaucoup de branches, car si on veut que le système
soit performant il faudra avoir en mémoire les topologies de chaque système de
fichiers.
L'implémentation de `mmap(2)` est nécessairement un cauchemar : lorsqu'un
fichier est modifié par deux processus qui le `mmap(2)`, on s'attend
normalement à voir les modifications dans les deux processus, or le premier à
faire une modification crée un nouveau fichier dans la branche accessible en
écriture. Il est alors ardu de réconcilier les pointeurs des deux processus.
D'une manière similaire, il faut penser à la gestion des *hard links* : tous
les pointeurs d'un contenu mis à jour devraient être modifiés dans la couche en
écriture, cependant il n'y a pas d'index des pointeurs, il n'est donc pas
facile de retrouver les fichiers à mettre à jour.
Ajoutons aussi que les systèmes de fichiers sous-jacents de chacune des
branches n'ont pas forcément les mêmes contraintes (tailles des noms de
fichiers, attributs étendus, métadonnées, encodage des accents, ...) et qu'il
faut réussir à jongler entre chaque, tout en retournant des erreurs cohérentes
le cas échéant.
Et bien d'autres encore. Notamment `readdir(2)` qui doit être stable malgré
les turbulences qui pourraient arriver entre deux appels, ...
Voir cette série d'articles résumant les différentes implémentations, leurs
choix et différences : <https://lwn.net/Articles/325369/>,
<https://lwn.net/Articles/327738/>.
{{% /card %}}
Par la suite, nous allons donc surtout nous concentrer sur le fonctionnement de ce système de fichier, en tentant autant que possible de faire le parallèle avec les autres.
## Les *whiteout files* en pratique
Avant tout, il faut savoir monter un tel système.
Voici un exemple général de création d'une union simple entre un système de fichiers en lecture seule et un en lecture/écriture :
```
mount -t overlay -olowerdir=/lower,upperdir=/upper,workdir=/work ignored /merged
```
Le type à utiliser est `overlay`, avec les options `lowerdir` qui indique
l'emplacement du/des dossiers à combiner en lecture seule (on les sépare par
des `:` lorsqu'il y en a plusieurs), on indique également le répertoire
contenant le système en lecture/écriture dans l'option `upperdir`, et il ne
faut pas oublier l'option `workdir` un chemin sur la même partition que
l'`upperdir`, qui doit être vide.
On termine l'appel par donner le périphérique source, qui est inutile dans
notre cas (`ignored` ou tout autre chaîne fera l'affaire), et enfin le dossier
vers lequel sera monté notre union : `/merged` dans l'exemple.
### Usage dans les conteneurs
Analysons un conteneur Docker en cours d'exécution pour en apprendre davantage.
D'abord, on vérifie que l'on utilise bien le *storage driver* `overlay2` :
```
42sh$ docker info | grep "Storage Driver"
Storage Driver: overlay2
```
C'est le cas (en fonction de la configuration de votre noyau, Docker aura
peut-être choisi un *driver* différent), commençons donc l'analyse :
```
42sh$ docker container run --rm -it debian
incntr$ mount | grep "on / "
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B62UNV3UB3X4TBWQMM6XCMM6W5:/var/lib/docker/overlay2/l/V6HGFN3C3PEW6CZ6XWRSHHDKJH,upperdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/diff,workdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/work)
```
On remarque que 2 `lowerdir` sont utilisés. Il s'agit de liens symboliques
pointant vers les dossiers identifiant les couches (les noms des liens sont
aléatoires, il s'agit en fait d'avoir un chemin raccourci par rapport au chemin
complet vers le système de fichiers de la couche, car le nombre de caractères
que l'on peut passer à l'appel système `mount(2)` est limité).
La branche la plus basse (le plus à droite du paramètre `lowerdir`) contient
l'unique couche de notre image `debian`, celle un peu plus à gauche superpose
un certain nombre de fichiers de configuration nécessaire à l'exécution du
conteneur (`/etc/hosts`, `resolv.conf`, ...).
La branche en lecture/écriture est également enregistrée dans le dossier
`/var/lib/docker/overlay2` et l'on peut voir son identifiant. L'`upperdir` se
trouve dans le dossier `diff`, tandis que le `workdir` est dans le dossier
`work`, sous le même identifiant de couche.
On peut également voir les dossiers utilisés en inspectant notre conteneur :
```
42sh$ docker container inspect youthful_wilbur | jq .[0].GraphDriver.Data
```
```json
{
"LowerDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31-init/diff:/var/lib/docker/overlay2/2cc3656c06...c0fb91d6/diff",
"MergedDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/merged",
"UpperDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/diff",
"WorkDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/work"
}
```
Si on teste avec une image ayant plus de couches, on obtient davantage de
`lowerdir`, un par couche. N'hésitez pas à faire la même série de commandes
avec l'image `python` par exemple.
### Ajout de fichiers
À ce stade, si nous regardons le contenu de notre dossier `upperdir`, nous
pouvons remarquer que celui-ci est vide. C'est normal puisque nous n'avons apporté
aucune modification.
Dans notre conteneur précédemment lancé, apportons une modification, en
ajoutant un fichier :
```
incntr$ echo "newfile" > /root/foobar
```
```
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
└── root
└── foobar
```
Notre nouveau fichier, qui n'est pourtant pas le seul dans l'arborescence que
l'on voit dans le conteneur, a été ajouté comme on pouvait s'y attendre, dans
la branche en lecture/écriture.
### Modification de fichiers
Si nous apportons une modification à un fichier, par exemple en ajoutant une
ligne, ce n'est pas seulement la différence qui est stockée dans la branche en
écriture, mais bien tout le fichier, tel qu'il a été modifié :
```
incntr$ echo "Bienvenue dans le conteneur" >> /etc/issue
```
```
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
└── etc
└── issue
```
```
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/issue
Debian GNU/Linux 11 \n \l
Bienvenue dans le conteneur
```
### Suppression de fichiers
Lorsque l'on souhaite supprimer un fichier que l'on vient d'ajouter, il n'y a
pas grand chose à faire puisque supprimer ce fichier de la branche en écriture
fera bien disparaître le fichier de l'arborescence montée.
Lorsqu'il s'agit de supprimer un fichier présent dans une branche en lecture
seule, il faut réussir à faire en sorte de masquer ce fichier au moyen d'un
marqueur. En fonction du *storage driver*, ce marqueur est différent : dans
`OverlayFS`, une suppression est matérialisée par un fichier spécial de type
caractère du même nom.
```
incntr$ rm /etc/adduser.conf
```
```
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
/var/lib/docker/overlay2/1531651afa872006a4b2b9b913d5d8ee317cf12be7883517ba77f3d094f871b4/diff
└── etc
└── adduser.conf
```
```
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
cat: No such device or address
42sh$ stat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
File: /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: fe0bh/65035d Inode: 515773 Links: 2 Device type: 0,0
```
Notons ici `Device type: 0,0`.
Pour créer nous-mêmes un fichier similaire, il faudrait utiliser :
```
42sh$ mkdir /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin
42sh$ mknod /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin/sh c 0 0
```
{{% card color="danger" title="Attention, comportement indéterminé !" %}}
Faire cette commande `mknod` alors que l'union de système de fichiers est
montée par ailleurs ne va pas faire disparaître le fichier `/bin/sh` car les
modifications qui pourraient être apportées aux branches en dehors du système
monté conduisent à des résultats explicitement indéfinis.
{{% /card %}}
### Suppressions pour `unionfs` et AuFS
Le concept de *whiteout file*, comme on a pu le voir, diffère en fonction du
système de fichiers. Il s'avère que même si l'OverlayFS a été intégré dans le
noyau Linux après maintes péripéties, Docker, lorsqu'a été spécifié le format
des archives utilisées pour distribuer les couches, utilise aujourd'hui le
format d'AuFS pour représenter les suppressions. Il est donc important de le
voir également.
Au lieu d'utiliser un fichier spécial, AuFS crée un fichier standard
`.wh.<filename>`, où `<filename>` est le nom du fichier à masquer.
Afin de s'adapter au *storage driver*, lors de la décompression de l'archive,
Docker s'emploie à convertir[^MOBYWHITEOUT] les *whiteout files* qu'il rencontre dans le
format attendu.
[^MOBYWHITEOUT]: Voir le code
<https://github.com/moby/moby/blob/master/pkg/archive/archive_linux.go#L27>
## Conclusion
Alors que vous pensiez peut-être ne pas vouloir savoir de quoi s'agissait les *whiteout files*, je suis sûr qu'à la lecture de cet article vous avez entrevu la complexité que revêtent tant les *union mounts* que les logiciels tirant parti de différentes implémentations.
Vous savez maintenant pourquoi il est notamment inutile de supprimer un gros fichier dans une autre couche que celle qui l'a apportée, par exemple :
```dockerfile
RUN wget https://dumps.wikimedia.org/enwiki/enwiki-pages-articles-multistream.xml.bz2
RUN ... # some other stuff
RUN rm enwiki-pages-articles-multistream.xml.bz2
```
Chaque `RUN` créant une couche distincte, notre fichier `enwiki-pages-articles-multistream.xml.bz2` sera distribué avec la première couche de notre image, puis un *whiteout file* sera inséré dans la couche correspondante au troisième `RUN`.