Récursion de lien symbolique - qu'est-ce qui la rend "réinitialisée"?

64

J'ai écrit un petit script bash pour voir ce qui se passe lorsque je continue à suivre un lien symbolique qui pointe vers le même répertoire. Je m'attendais à ce que soit un répertoire de travail très long, soit un plantage. Mais le résultat m'a surpris ...

mkdir a
cd a

ln -s ./. a

for i in `seq 1 1000`
do
  cd a
  pwd
done

Une partie de la sortie est

${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a/a
${HOME}/a
${HOME}/a/a
${HOME}/a/a/a
${HOME}/a/a/a/a
${HOME}/a/a/a/a/a
${HOME}/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a
${HOME}/a/a/a/a/a/a/a/a

Que se passe-t-il ici?

Lucas
la source

Réponses:

88

Patrice a identifié la source du problème dans sa réponse , mais si vous voulez savoir comment en arriver à pourquoi vous obtenez cela, voici la longue histoire.

Le répertoire de travail actuel d'un processus n'est pas ce que vous penseriez trop compliqué. C'est un attribut du processus qui est un handle vers un fichier de type répertoire d'où partent les chemins relatifs (dans les appels système effectués par le processus). Lors de la résolution d’un chemin relatif, le noyau n’a pas besoin de connaître le chemin (a) complet de ce répertoire en cours, il lit simplement les entrées du répertoire dans ce fichier répertoire pour trouver le premier composant du chemin relatif (et ..ressemble à tout autre déposer à cet égard) et continue à partir de là.

En tant qu'utilisateur, vous souhaitez parfois savoir où se trouve ce répertoire dans l'arborescence. Avec la plupart des Unices, l’arborescence de répertoires est une arborescence, sans boucle. C'est-à-dire qu'il n'y a qu'un seul chemin depuis la racine de tree ( /) vers un fichier donné. Ce chemin s'appelle généralement le chemin canonique.

Pour obtenir le chemin du répertoire de travail actuel, ce processus doit faire est tout simplement marcher (bien vers le bas si vous voulez voir un arbre avec sa racine au fond) l'arbre jusqu'à la racine, trouver les noms des noeuds en chemin.

Par exemple, un processus essayant de savoir que son répertoire actuel est /a/b/c, ouvrirait le ..répertoire (le chemin relatif, ainsi que ..l'entrée dans le répertoire actuel) et rechercherait un fichier de type répertoire avec le même numéro d'inode que ., découvrez que ccorrespond, puis s'ouvre ../..et ainsi de suite jusqu'à ce qu'il trouve /. Il n'y a pas d'ambiguïté là-bas.

C’est ce que les fonctions getwd()ou getcwd()C font ou ont au moins l'habitude de faire.

Sur certains systèmes tels que Linux moderne, un appel système renvoie le chemin canonique du répertoire actuel qui effectue cette recherche dans l'espace du noyau (et vous permet de rechercher votre répertoire actuel même si vous n'avez pas accès en lecture à tous ses composants). , et c'est ce qui getcwd()appelle là. Sur Linux moderne, vous pouvez également trouver le chemin du répertoire actuel via un readlink () sur /proc/self/cwd.

C'est ce que font la plupart des langues et des premiers shells lorsqu'ils renvoient le chemin d'accès au répertoire actuel.

Dans votre cas, vous pouvez appeler cd aque peut fois que vous voulez, parce que c'est un lien symbolique vers .le répertoire en cours ne change pas tous getcwd(), pwd -P, python -c 'import os; print os.getcwd()', perl -MPOSIX -le 'print getcwd'renverrait votre ${HOME}.

Maintenant, les liens symboliques ont compliqué tout cela.

symlinksautoriser les sauts dans l'arborescence. Dans /a/b/c, si /aou /a/bou /a/b/cest un lien symbolique, le chemin canonique de /a/b/cserait quelque chose de complètement différent. En particulier, l' ..entrée /a/b/cn'est pas nécessairement /a/b.

Dans le shell Bourne, si vous le faites:

cd /a/b/c
cd ..

Ou même:

cd /a/b/c/..

Il n'y a aucune garantie que vous finirez dans /a/b.

Juste comme:

vi /a/b/c/../d

n'est pas nécessairement la même chose que:

vi /a/b/d

kshintroduit un concept de répertoire de travail actuel logique pour contourner ce problème. Les gens s'y sont habitués et POSIX a fini par spécifier ce comportement, ce qui signifie que la plupart des obus le font aussi:

Pour les commandes internes cdet pwdintégrées ( et uniquement pour celles-ci (mais également pour popd/ pushdsur les shells qui en ont)), le shell conserve sa propre idée du répertoire de travail en cours. Il est stocké dans la $PWDvariable spéciale.

Quand tu fais:

cd c/d

même si cou c/dsont des liens symboliques, tout en $PWDcontaines /a/b, il ajoute c/dà la fin , donc $PWDdevient /a/b/c/d. Et quand tu fais:

cd ../e

Au lieu de le faire chdir("../e"), c'est le cas chdir("/a/b/c/e").

Et la pwdcommande ne renvoie que le contenu de la $PWDvariable.

C'est utile dans les shells interactifs , car en pwdsortie un chemin vers le répertoire courant qui donne des informations sur la façon dont vous y êtes arrivé et aussi longtemps que vous utilisez uniquement ..dans les arguments pour cdet pas les autres commandes, il est moins susceptible de vous surprendre, car cd a; cd ..ou cd a/..serait généralement vous retourner à l'endroit où vous étiez.

Maintenant, $PWDn'est pas modifié, sauf si vous faites un cd. Jusqu'à la prochaine fois que vous appelez cdou pwd, beaucoup de choses pourraient arriver, n'importe lequel des composants de $PWDpourrait être renommé. Le répertoire en cours ne change jamais (c'est toujours le même inode, bien qu'il puisse être supprimé), mais son chemin dans l'arborescence des répertoires peut changer complètement. getcwd()calcule le répertoire en cours chaque fois qu'il est appelé en descendant dans l'arborescence afin que ses informations soient toujours exactes, mais pour le répertoire logique implémenté par les shells POSIX, les informations contenues $PWDpeuvent devenir obsolètes. Donc, en courant cdou pwd, certains obus peuvent vouloir se protéger de cela.

Dans ce cas particulier, vous voyez différents comportements avec différentes coques.

Certains, comme ceux qui ksh93ignorent complètement le problème, renverront des informations incorrectes même après votre appel cd(et vous ne verrez pas le comportement que vous observez bashlà-bas).

Certains aiment bashou zshvérifient que $PWDc'est toujours un chemin d'accès au répertoire courant sur cd, mais pas sur pwd.

pdksh vérifie les deux pwdet cd(mais sur pwd, ne met pas à jour $PWD)

ash(au moins celui trouvé sur Debian) ne vérifie pas, et quand vous le faites cd a, en fait cd "$PWD/a", donc si le répertoire actuel a changé et $PWDne pointe plus vers le répertoire actuel, il ne sera pas changé dans le arépertoire du répertoire actuel. , mais celui dans $PWD(et retourne une erreur s'il n'existe pas).

Si vous voulez jouer avec, vous pouvez faire:

cd
mkdir -p a/b
cd a
pwd
mv ~/a ~/b 
pwd
echo "$PWD"
cd b
pwd; echo "$PWD"; pwd -P # (and notice the bug in ksh93)

dans divers coquillages.

Dans votre cas, puisque vous utilisez bash, après un cd a, des bashvérifications qui $PWDpointent toujours vers le répertoire en cours. Pour ce faire, il appelle stat()la valeur de $PWDvérifier son numéro d'inode et le comparer à celui de ..

Mais lorsque la recherche du $PWDchemin implique la résolution d'un trop grand nombre de liens symboliques, le système stat()renvoie une erreur. Le shell ne peut donc pas vérifier s'il $PWDcorrespond toujours au répertoire actuel. Il le calcule à nouveau avec getcwd()et le met à jour en $PWDconséquence.

Maintenant, pour clarifier la réponse de Patrice, cette vérification du nombre de liens symboliques rencontrés lors de la recherche d'un chemin est une protection contre les boucles de liens symboliques. La boucle la plus simple peut être réalisée avec

rm -f a b
ln -s a b
ln -s b a

Sans cette protection, cd a/xle système devrait trouver où les aliens, les trouver, bsont un lien symbolique vers lequel les liens ase poursuivraient indéfiniment. Le moyen le plus simple de se prémunir contre cela consiste à abandonner après avoir résolu plus d'un nombre arbitraire de liens symboliques.

Revenons maintenant au répertoire de travail actuel logique et pourquoi ce n'est pas une si bonne fonctionnalité. Il est important de réaliser que c'est uniquement pour cdle shell et non pour d'autres commandes.

Par exemple:

cd -- "$dir" &&  vi -- "$file"

n'est pas toujours la même chose que:

vi -- "$dir/$file"

C'est pourquoi vous trouverez parfois que les utilisateurs recommandent de toujours utiliser des cd -Pscripts pour éviter toute confusion (vous ne voulez pas que votre logiciel traite un argument de manière ../xdifférente des autres commandes simplement parce qu'il est écrit dans un shell plutôt que dans un autre langage).

L' -Poption est de désactiver le répertoire logique de manipulation alors cd -P -- "$var"ne fait appel chdir()sur le contenu $var(sauf quand $varest -mais c'est une autre histoire). Et après un cd -P, $PWDcontiendra un chemin canonique.

Stéphane Chazelas
la source
7
Doux Jésus! Merci pour cette réponse complète, c'est vraiment très intéressant :)
Lucas
Réponse géniale, merci beaucoup! Je sens que je peu connaissais toutes ces choses, mais je ne l' avais jamais compris ou pensé à la façon dont ils sont tous venus ensemble. Grande explication.
Dimo414
42

Ceci est le résultat d'une limite codée en dur dans la source du noyau Linux; pour empêcher le déni de service, la limite du nombre de liens symboliques imbriqués est de 40 (trouvée dans la follow_link()fonction interne fs/namei.c, appelée par nested_symlink()dans la source du noyau).

Vous obtiendrez probablement un comportement similaire (et éventuellement une autre limite que 40) avec d'autres noyaux supportant des liens symboliques.

Patrice levesque
la source
1
Y a-t-il une raison pour qu'elle "réinitialise" plutôt que de simplement s'arrêter. c'est-à-dire x%40plutôt que max(x,40). Je suppose que vous pouvez toujours voir que vous avez changé de répertoire.
Lucas
4
Un lien vers la source, pour tous les curieux: lxr.linux.no/linux+v3.9.6/fs/namei.c#L818
Ben