Skip to content

Commit 35282a5

Browse files
committed
linux: add src-nofollow & dest-nofollow options
Introduce `src-nofollow` and `dest-nofollow` bind mount options for more precise control over symbolic link handling. The `src-nofollow` option enables mounting the source symbolic link itself, rather than its target. The `dest-nofollow` option ensures that if the destination path is a symbolic link, the mount operation replaces the symbolic link itself, instead of dereferencing it and mounting to its target. Closes: containers#1761 Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
1 parent 2619cbb commit 35282a5

5 files changed

Lines changed: 155 additions & 8 deletions

File tree

crun.1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,16 @@ 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 dest-nofollow
665+
When this option is specified for a bind mount, and the destination of
666+
the bind mount is a symbolic link, \fBcrun\fR will mount the symbolic link
667+
itself at the target destination.
668+
669+
.SH src-nofollow
670+
When this option is specified for a bind mount, and the source of the
671+
bind mount is a symbolic link, \fBcrun\fR will use the symlink itself
672+
rather than the file or directory the symbolic link points to.
673+
664674
.SH r$FLAG mount options
665675
If a \fBr$FLAG\fR mount option is specified then the flag \fB$FLAG\fR is set
666676
recursively for each children mount.

crun.1.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,16 @@ 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+
## dest-nofollow
575+
When this option is specified for a bind mount, and the destination of
576+
the bind mount is a symbolic link, `crun` will mount the symbolic link
577+
itself at the target destination.
578+
579+
## src-nofollow
580+
When this option is specified for a bind mount, and the source of the
581+
bind mount is a symbolic link, `crun` will use the symlink itself
582+
rather than the file or directory the symbolic link points to.
583+
574584
## r$FLAG mount options
575585

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

src/libcrun/linux.c

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2130,14 +2130,27 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
21302130
path = proc_buf;
21312131
}
21322132

2133-
ret = get_file_type (&src_mode, (extra_flags & OPTION_COPY_SYMLINK) ? true : false, path);
2133+
if ((extra_flags & OPTION_COPY_SYMLINK) && (extra_flags & (OPTION_SRC_NOFOLLOW | OPTION_DEST_NOFOLLOW)))
2134+
return crun_make_error (err, 0, "`copy-symlink` is mutually exclusive with `src-nofollow` and `dest-nofollow`");
2135+
2136+
/* Do not resolve the symlink only when src-nofollow and copy-symlink are used. */
2137+
ret = get_file_type (&src_mode, (extra_flags & (OPTION_SRC_NOFOLLOW | OPTION_COPY_SYMLINK)) ? true : false, path);
21342138
if (UNLIKELY (ret < 0))
21352139
return crun_make_error (err, errno, "cannot stat `%s`", path);
21362140

2141+
if (S_ISLNK (src_mode) && (extra_flags & OPTION_DEST_NOFOLLOW) && source_mountfd < 0)
2142+
{
2143+
ret = get_bind_mount (AT_FDCWD, def->mounts[i]->source, true, true, extra_flags & OPTION_SRC_NOFOLLOW, err);
2144+
if (UNLIKELY (ret < 0))
2145+
return ret;
2146+
2147+
source_mountfd = ret;
2148+
}
2149+
21372150
data = append_mode_if_missing (data, "mode=1755");
21382151
}
21392152

2140-
if (S_ISLNK (src_mode))
2153+
if (S_ISLNK (src_mode) && (extra_flags & OPTION_COPY_SYMLINK))
21412154
{
21422155
cleanup_free char *target = NULL;
21432156
ssize_t len;
@@ -2185,12 +2198,22 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
21852198
{
21862199
bool is_dir = S_ISDIR (src_mode);
21872200

2188-
/* Make sure any other directory/file is created and take a O_PATH reference to it. */
2189-
ret = crun_safe_create_and_open_ref_at (is_dir, get_private_data (container)->rootfsfd, rootfs, target, is_dir ? 01755 : 0755, err);
2190-
if (UNLIKELY (ret < 0))
2191-
return ret;
2192-
2193-
targetfd = ret;
2201+
if (extra_flags & OPTION_DEST_NOFOLLOW)
2202+
{
2203+
/* If dest-nofollow is specified, expect the target to exist. */
2204+
ret = safe_openat (get_private_data (container)->rootfsfd, rootfs, target, O_PATH | O_NOFOLLOW | O_CLOEXEC, 0, err);
2205+
if (UNLIKELY (ret < 0))
2206+
return ret;
2207+
targetfd = ret;
2208+
}
2209+
else
2210+
{
2211+
/* Make sure any other directory/file is created and take a O_PATH reference to it. */
2212+
ret = crun_safe_create_and_open_ref_at (is_dir, get_private_data (container)->rootfsfd, rootfs, target, is_dir ? 01755 : 0755, err);
2213+
if (UNLIKELY (ret < 0))
2214+
return ret;
2215+
targetfd = ret;
2216+
}
21942217
}
21952218

21962219
if (extra_flags & OPTION_TMPCOPYUP)

tests/test_mounts.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,106 @@ def test_mount_help():
703703

704704
return 0
705705

706+
def test_bind_mount_symlink_nofollow():
707+
root = get_tests_root()
708+
file_target = os.path.join(root, "a-file")
709+
symlink = os.path.join(root, "a-symlink")
710+
target_content = file_target
711+
file_target_content = "inside-the-file"
712+
713+
with open(file_target, "w+") as f:
714+
f.write(file_target_content)
715+
716+
os.symlink(target_content, symlink)
717+
718+
def prepare_rootfs(rootfs):
719+
path = os.path.join(rootfs, "target")
720+
os.symlink("point-to-nowhere", path)
721+
722+
for userns in [True, False]:
723+
for src_nofollow in [True, False]:
724+
conf = base_config()
725+
add_all_namespaces(conf, userns=userns)
726+
727+
if userns:
728+
getMapping = lambda x : [
729+
{
730+
"containerID": 0,
731+
"hostID": x,
732+
"size": 1
733+
}
734+
]
735+
conf['linux']['uidMappings'] = getMapping(os.geteuid())
736+
conf['linux']['gidMappings'] = getMapping(os.getegid())
737+
738+
if src_nofollow:
739+
options = ["bind", "dest-nofollow", "src-nofollow"]
740+
conf['process']['args'] = ['/init', 'readlink', '/target']
741+
expected = target_content
742+
else:
743+
options = ["bind", "dest-nofollow"]
744+
conf['process']['args'] = ['/init', 'cat', '/target']
745+
expected = file_target_content
746+
747+
mount_opt = {"destination": "/target", "type": "bind", "source": symlink, "options": options}
748+
conf['mounts'].append(mount_opt)
749+
750+
try:
751+
out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs)
752+
sys.stderr.write("got output %s with configuration userns=%s, src-nofollow=%s\n" % (out, userns, src_nofollow))
753+
if expected not in out:
754+
return -1
755+
except Exception as e:
756+
sys.stderr.write("error %s\n" % e)
757+
return -1
758+
759+
return 0
760+
761+
def test_bind_mount_file_nofollow():
762+
root = get_tests_root()
763+
target = os.path.join(root, "a-file")
764+
target_content = "content-of-file"
765+
766+
with open(target, "w+") as f:
767+
f.write(target_content)
768+
769+
def prepare_rootfs(rootfs):
770+
path = os.path.join(rootfs, "symlink")
771+
os.symlink("point-to-nowhere", path)
772+
773+
for userns in [True, False]:
774+
for src_nofollow in [True, False]:
775+
conf = base_config()
776+
conf['process']['args'] = ['/init', 'cat', '/symlink']
777+
add_all_namespaces(conf, userns=userns)
778+
779+
if userns:
780+
getMapping = lambda x : [
781+
{
782+
"containerID": 0,
783+
"hostID": x,
784+
"size": 1
785+
}
786+
]
787+
conf['linux']['uidMappings'] = getMapping(os.geteuid())
788+
conf['linux']['gidMappings'] = getMapping(os.getegid())
789+
790+
if src_nofollow:
791+
options = ["bind", "dest-nofollow", "src-nofollow"]
792+
else:
793+
options = ["bind", "dest-nofollow"]
794+
mount_opt = {"destination": "/symlink", "type": "bind", "source": target, "options": options}
795+
conf['mounts'].append(mount_opt)
796+
797+
try:
798+
out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs)
799+
sys.stderr.write("got output %s with configuration userns=%s, src-nofollow=%s\n" % (out, userns, src_nofollow))
800+
if target_content not in out:
801+
return 1
802+
except Exception as e:
803+
sys.stderr.write("error %s\n" % e)
804+
return 0
805+
706806
all_tests = {
707807
"mount-ro" : test_mount_ro,
708808
"mount-rro" : test_mount_rro,
@@ -732,6 +832,8 @@ def test_mount_help():
732832
"mount-ro-cgroup": test_ro_cgroup,
733833
"mount-cgroup-without-netns": test_cgroup_mount_without_netns,
734834
"mount-copy-symlink": test_copy_symlink,
835+
"mount-bind-mount-symlink-nofollow": test_bind_mount_symlink_nofollow,
836+
"mount-bind-mount-file-nofollow": test_bind_mount_file_nofollow,
735837
"mount-tmpfs-permissions": test_mount_tmpfs_permissions,
736838
"mount-add-remove-mounts": test_add_remove_mounts,
737839
"mount-help": test_mount_help,

tests/test_oci_features.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ def test_crun_features():
106106
"defaults",
107107
"async",
108108
"rasync",
109+
"dest-nofollow",
110+
"src-nofollow",
109111
"private",
110112
"tmpcopyup",
111113
"rexec",

0 commit comments

Comments
 (0)