Skip to content

Commit 8315caf

Browse files
committed
linux: add 'at_symlink_nofollow' bind mount option
This option enables bind-mounting a source symbolic link itself (not its target) onto a destination path. If the destination path is also a symbolic link, it is replaced by the mount rather than being dereferenced. This is useful for precisely controlling symlink handling during mounts, such as when needing to overlay a container's symlink with a mount of a host's symlink. The implementation ensures that if the source is a symlink and this option is active, the symlink's nature is preserved in the mount. Closes: containers#1761 Signed-off-by: Giuseppe Scrivano <[email protected]>
1 parent 851f46e commit 8315caf

File tree

4 files changed

+110
-35
lines changed

4 files changed

+110
-35
lines changed

crun.1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,17 @@ destination instead of attempting a mount that would resolve the
661661
symlink itself. If the destination already exists and it is not a
662662
symlink with the expected content, crun will return an error.
663663

664+
.SH at_symlink_nofollow
665+
When this option is specified for a bind mount, and the source of the
666+
bind mount is a symbolic link, \fBcrun\fR will mount the symbolic link
667+
itself at the target destination, rather than the file or directory
668+
the symbolic link points to. This is achieved by using the
669+
\fBAT_SYMLINK_NOFOLLOW\fR flag during the mount operation, specifically
670+
with the \fBopen_tree(2)\fR system call in conjunction with
671+
\fBOPEN_TREE_CLONE\fR\&. If the source of the bind mount is not a symbolic
672+
link, this option has no practical effect on the mount's target but
673+
will still be passed to \fBopen_tree(2)\fR\&.
674+
664675
.SH r$FLAG mount options
665676
If a \fBr$FLAG\fR mount option is specified then the flag \fB$FLAG\fR is set
666677
recursively for each children mount.

crun.1.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,17 @@ destination instead of attempting a mount that would resolve the
571571
symlink itself. If the destination already exists and it is not a
572572
symlink with the expected content, crun will return an error.
573573

574+
## at_symlink_nofollow
575+
When this option is specified for a bind mount, and the source of the
576+
bind mount is a symbolic link, `crun` will mount the symbolic link
577+
itself at the target destination, rather than the file or directory
578+
the symbolic link points to. This is achieved by using the
579+
`AT_SYMLINK_NOFOLLOW` flag during the mount operation, specifically
580+
with the `open_tree(2)` system call in conjunction with
581+
`OPEN_TREE_CLONE`. If the source of the bind mount is not a symbolic
582+
link, this option has no practical effect on the mount's target but
583+
will still be passed to `open_tree(2)`.
584+
574585
## r$FLAG mount options
575586

576587
If a `r$FLAG` mount option is specified then the flag `$FLAG` is set

src/libcrun/linux.c

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,7 +2116,7 @@ do_mounts (libcrun_container_t *container, int rootfsfd, const char *rootfs, lib
21162116
path = proc_buf;
21172117
}
21182118

2119-
ret = get_file_type (&src_mode, (extra_flags & OPTION_COPY_SYMLINK) ? true : false, path);
2119+
ret = get_file_type (&src_mode, (extra_flags & (OPTION_AT_SYMLINK_NOFOLLOW | OPTION_COPY_SYMLINK)) ? true : false, path);
21202120
if (UNLIKELY (ret < 0))
21212121
return crun_make_error (err, errno, "cannot stat `%s`", path);
21222122

@@ -2125,20 +2125,45 @@ do_mounts (libcrun_container_t *container, int rootfsfd, const char *rootfs, lib
21252125

21262126
if (S_ISLNK (src_mode))
21272127
{
2128-
cleanup_free char *target = NULL;
2129-
ssize_t len;
2128+
if (extra_flags & OPTION_COPY_SYMLINK)
2129+
{
2130+
cleanup_free char *target = NULL;
2131+
ssize_t len;
21302132

2131-
/* If we got here, it means the OPTION_COPY_SYMLINK was provided, so we need to copy the origin
2132-
symlink instead of performing the mount operation. */
2133-
len = safe_readlinkat (AT_FDCWD, def->mounts[i]->source, &target, 0, err);
2134-
if (UNLIKELY (len < 0))
2135-
return len;
2133+
/* If we got here, it means the OPTION_COPY_SYMLINK was provided, so we need to copy the origin
2134+
symlink instead of performing the mount operation. */
2135+
len = safe_readlinkat (AT_FDCWD, def->mounts[i]->source, &target, 0, err);
2136+
if (UNLIKELY (len < 0))
2137+
return len;
21362138

2137-
ret = safe_create_symlink (rootfsfd, rootfs, target, def->mounts[i]->destination, err);
2138-
if (UNLIKELY (ret < 0))
2139-
return ret;
2139+
ret = safe_create_symlink (rootfsfd, rootfs, target, def->mounts[i]->destination, err);
2140+
if (UNLIKELY (ret < 0))
2141+
return ret;
2142+
2143+
mounted = true;
2144+
}
2145+
else if (extra_flags & OPTION_AT_SYMLINK_NOFOLLOW)
2146+
{
2147+
cleanup_close int srcfd = -1;
21402148

2141-
mounted = true;
2149+
ret = get_bind_mount (AT_FDCWD, def->mounts[i]->source, true, true, true, err);
2150+
if (UNLIKELY (ret < 0))
2151+
return ret;
2152+
2153+
srcfd = ret;
2154+
2155+
ret = safe_openat (rootfsfd, rootfs, target, O_PATH | O_NOFOLLOW | O_CLOEXEC, 0, err);
2156+
if (UNLIKELY (ret < 0))
2157+
return ret;
2158+
2159+
targetfd = ret;
2160+
2161+
ret = fs_move_mount_to (srcfd, targetfd, NULL);
2162+
if (UNLIKELY (ret < 0))
2163+
return crun_make_error (err, errno, "move mount to `%s`", def->mounts[i]->destination);
2164+
2165+
mounted = true;
2166+
}
21422167
}
21432168
else if (is_sysfs_or_proc)
21442169
{

tests/test_mounts.py

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,20 +124,20 @@ def test_ro_cgroup():
124124
add_all_namespaces(conf, cgroupns=cgroupns, netns=netns)
125125
mounts = [
126126
{
127-
"destination": "/sys",
128-
"type": "sysfs",
129-
"source": "sysfs",
130-
"options": [
131-
"nosuid",
132-
"noexec",
133-
"nodev",
134-
"ro"
135-
]
136-
},
127+
"destination": "/sys",
128+
"type": "sysfs",
129+
"source": "sysfs",
130+
"options": [
131+
"nosuid",
132+
"noexec",
133+
"nodev",
134+
"ro"
135+
]
136+
},
137137
{
138-
"destination": "/proc",
139-
"type": "proc"
140-
}
138+
"destination": "/proc",
139+
"type": "proc"
140+
}
141141
]
142142

143143
if has_cgroup_mount:
@@ -556,28 +556,28 @@ def test_cgroup_mount_without_netns():
556556
add_all_namespaces(conf, cgroupns=cgroupns, netns=False)
557557
mounts = [
558558
{
559-
"destination": "/proc",
560-
"type": "proc"
561-
},
559+
"destination": "/proc",
560+
"type": "proc"
561+
},
562562
{
563-
"destination": "/sys",
564-
"type": "bind",
565-
"source": "/sys",
566-
"options": [
563+
"destination": "/sys",
564+
"type": "bind",
565+
"source": "/sys",
566+
"options": [
567567
"rprivate",
568568
"nosuid",
569569
"noexec",
570570
"nodev",
571571
"ro",
572572
"rbind"
573-
]
574-
},
573+
]
574+
},
575575
{
576576
"destination": "/sys/fs/cgroup",
577577
"type": "cgroup",
578578
"source": "cgroup",
579579
"options": [
580-
"rprivate",
580+
"rprivate",
581581
"nosuid",
582582
"noexec",
583583
"nodev",
@@ -676,6 +676,33 @@ def test_mount_help():
676676

677677
return 0
678678

679+
def test_bind_mount_symlink_nofollow():
680+
root = get_tests_root()
681+
symlink = os.path.join(root, "a-symlink")
682+
target_content = "content-of-symlink"
683+
684+
os.symlink(target_content, symlink)
685+
686+
def prepare_rootfs(rootfs):
687+
path = os.path.join(rootfs, "symlink")
688+
os.symlink("point-to-nowhere", path)
689+
690+
conf = base_config()
691+
conf['process']['args'] = ['/init', 'readlink', '/symlink']
692+
add_all_namespaces(conf)
693+
mount_opt = {"destination": "/symlink", "type": "bind", "source": symlink, "options": ["bind", "at_symlink_nofollow"]}
694+
conf['mounts'].append(mount_opt)
695+
696+
try:
697+
out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs)
698+
sys.stderr.write("got output %s\n" % out)
699+
if target_content in out:
700+
return 0
701+
except Exception as e:
702+
sys.stderr.write("error %s\n" % e)
703+
pass
704+
return -1
705+
679706
all_tests = {
680707
"mount-ro" : test_mount_ro,
681708
"mount-rro" : test_mount_rro,
@@ -703,6 +730,7 @@ def test_mount_help():
703730
"mount-ro-cgroup": test_ro_cgroup,
704731
"mount-cgroup-without-netns": test_cgroup_mount_without_netns,
705732
"mount-copy-symlink": test_copy_symlink,
733+
"mount-bind-mount-symlink-nofollow": test_bind_mount_symlink_nofollow,
706734
"mount-tmpfs-permissions": test_mount_tmpfs_permissions,
707735
"mount-add-remove-mounts": test_add_remove_mounts,
708736
"mount-help": test_mount_help,

0 commit comments

Comments
 (0)