mount(2)
systémové volání zcela vyřeší své cesty přes připojení a symbolické odkazy, ale na rozdíl od open(2)
, nepřijme cestu ke smazanému souboru, tj. cestu, která vede k neodpojené položce adresáře.
(podobně jako <filename> (deleted)
cesty /proc/PID/fd/FD
, procfs zobrazí nepropojené dentry jako <filename>//deleted
v /proc/PID/mountinfo
)
# unshare -m
# echo foo > foo; touch bar baz quux
# mount -B foo bar
# mount -B bar baz
# grep foo /proc/self/mountinfo
56 38 8:7 /tmp/foo /tmp/bar ...
57 38 8:7 /tmp/foo /tmp/baz ...
# rm foo
# grep foo /proc/self/mountinfo
56 38 8:7 /tmp/foo//deleted /tmp/bar ...
57 38 8:7 /tmp/foo//deleted /tmp/baz ...
# mount -B baz quux
mount: mount(2) failed: /tmp/quux: No such file or directory
To vše dříve fungovalo ve starších jádrech, ale ne od verze 4.19, která byla poprvé představena touto změnou:
commit 1064f874abc0d05eeed8993815f584d847b72486
Author: Eric W. Biederman <[email protected]>
Date: Fri Jan 20 18:28:35 2017 +1300
mnt: Tuck mounts under others instead of creating shadow/side mounts.
...
+ /* Preallocate a mountpoint in case the new mounts need
+ * to be tucked under other mounts.
+ */
+ smp = get_mountpoint(source_mnt->mnt.mnt_root);
+ if (IS_ERR(smp))
+ return PTR_ERR(smp);
+
Zdá se, že tento efekt byl změnou nezamýšlený. Od té doby se nahromadily další nesouvisející změny, které to ještě více zamotaly.
Důsledkem toho je, že také zabraňuje připnutí smazaného souboru někam jinam do jmenného prostoru prostřednictvím otevřeného fd:
# exec 7>foo; touch bar
# rm foo
# mount -B /proc/self/fd/7 bar
mount: mount(2) failed: /tmp/bar: No such file or directory
Poslední příkaz selže kvůli stejné podmínce jako OP.
Můžete dokonce znovu vytvořit
a
, ukazující na stejný přesný inode, ale dostanete to samé
Je to stejné jako u /proc/PID/fd/FD
„symlinky“. Jádro je dostatečně chytré na to, aby sledovalo soubor přes přímé přejmenování, ale ne přes ln
+ rm
(link(2)
+ unlink(2)
):
# unshare -m
# echo foo > foo; touch bar baz
# mount -B foo bar
# mount -B bar baz
# grep foo /proc/self/mountinfo
56 38 8:7 /tmp/foo /tmp/bar ...
57 38 8:7 /tmp/foo /tmp/baz ...
# mv foo quux
# grep bar /proc/self/mountinfo
56 38 8:7 /tmp/quux /tmp/bar ...
# ln quux foo; rm quux
# grep bar /proc/self/mountinfo
56 38 8:7 /tmp/quux//deleted /tmp/bar ...
Když jsem procházel zdrojový kód, našel jsem přesně jeden ENOENT
to bylo relevantní, tj. pro nepropojenou položku adresáře:
static int attach_recursive_mnt(struct mount *source_mnt,
struct mount *dest_mnt,
struct mountpoint *dest_mp,
struct path *parent_path)
{
[...]
/* Preallocate a mountpoint in case the new mounts need
* to be tucked under other mounts.
*/
smp = get_mountpoint(source_mnt->mnt.mnt_root);
static struct mountpoint *get_mountpoint(struct dentry *dentry)
{
struct mountpoint *mp, *new = NULL;
int ret;
if (d_mountpoint(dentry)) {
/* might be worth a WARN_ON() */
if (d_unlinked(dentry))
return ERR_PTR(-ENOENT);
https://elixir.bootlin.com/linux/v5.2/source/fs/namespace.c#L3100
get_mountpoint()
se obecně aplikuje na cíl, nikoli na zdroj. V této funkci je volána kvůli mount propagation. Během šíření připojení je nutné vynutit pravidlo, že na smazaný soubor nelze přidávat připojení. Ale vynucení probíhá dychtivě, i když nedojde k žádnému šíření mountů, které by to vyžadovalo. Myslím, že je dobré, že je kontrola takto konzistentní, jen je kódovaná trochu nejasněji, než bych v ideálním případě preferoval.
Ať se na to dívám jakkoliv, myslím si, že je rozumné to prosadit. Pokud to pomůže snížit počet podivných případů k analýze a nikdo nebude mít zvlášť přesvědčivý protiargument.