Skip to content

Commit 6d58afd

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 0a33a11 commit 6d58afd

5 files changed

Lines changed: 162 additions & 35 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: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,6 +2070,7 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
20702070
for (i = 0; i < def->mounts_len; i++)
20712071
{
20722072
const char *target = consume_slashes (def->mounts[i]->destination);
2073+
cleanup_close int source_mountfd = -1;
20732074
cleanup_free char *data = NULL;
20742075
char *type;
20752076
char *source;
@@ -2083,6 +2084,9 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
20832084
uint64_t rec_clear = 0;
20842085
uint64_t rec_set = 0;
20852086

2087+
if (mount_fds)
2088+
source_mountfd = get_and_reset (&(mount_fds->fds[i]));
2089+
20862090
type = def->mounts[i]->type;
20872091

20882092
if (def->mounts[i]->options == NULL)
@@ -2126,14 +2130,29 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
21262130
path = proc_buf;
21272131
}
21282132

2129-
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);
21302138
if (UNLIKELY (ret < 0))
21312139
return crun_make_error (err, errno, "cannot stat `%s`", path);
21322140

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

2136-
if (S_ISLNK (src_mode))
2155+
if (S_ISLNK (src_mode) && (extra_flags & OPTION_COPY_SYMLINK))
21372156
{
21382157
cleanup_free char *target = NULL;
21392158
ssize_t len;
@@ -2181,12 +2200,22 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
21812200
{
21822201
bool is_dir = S_ISDIR (src_mode);
21832202

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

21922221
if (extra_flags & OPTION_TMPCOPYUP)
@@ -2206,15 +2235,14 @@ do_mounts (libcrun_container_t *container, const char *rootfs, libcrun_error_t *
22062235
source = def->mounts[i]->source ? def->mounts[i]->source : type;
22072236

22082237
/* Check if there is already a mount for the requested file system. */
2209-
if (! mounted && mount_fds && mount_fds->fds[i] >= 0)
2238+
if (! mounted && source_mountfd >= 0)
22102239
{
2211-
cleanup_close int mfd = get_and_reset (&(mount_fds->fds[i]));
22122240

2213-
ret = fs_move_mount_to (mfd, targetfd, NULL);
2241+
ret = fs_move_mount_to (source_mountfd, targetfd, NULL);
22142242
if (LIKELY (ret == 0))
22152243
{
22162244
/* Force no MS_BIND flag to not attempt again the bind mount. */
2217-
ret = do_mount (container, NULL, mfd, target, NULL, flags & ~MS_BIND, data, LABEL_NONE, err);
2245+
ret = do_mount (container, NULL, source_mountfd, target, NULL, flags & ~MS_BIND, data, LABEL_NONE, err);
22182246
if (UNLIKELY (ret < 0))
22192247
return ret;
22202248
mounted = true;

tests/test_mounts.py

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -151,20 +151,20 @@ def test_ro_cgroup():
151151
add_all_namespaces(conf, cgroupns=cgroupns, netns=netns)
152152
mounts = [
153153
{
154-
"destination": "/sys",
155-
"type": "sysfs",
156-
"source": "sysfs",
157-
"options": [
158-
"nosuid",
159-
"noexec",
160-
"nodev",
161-
"ro"
162-
]
163-
},
154+
"destination": "/sys",
155+
"type": "sysfs",
156+
"source": "sysfs",
157+
"options": [
158+
"nosuid",
159+
"noexec",
160+
"nodev",
161+
"ro"
162+
]
163+
},
164164
{
165-
"destination": "/proc",
166-
"type": "proc"
167-
}
165+
"destination": "/proc",
166+
"type": "proc"
167+
}
168168
]
169169

170170
if has_cgroup_mount:
@@ -583,28 +583,28 @@ def test_cgroup_mount_without_netns():
583583
add_all_namespaces(conf, cgroupns=cgroupns, netns=False)
584584
mounts = [
585585
{
586-
"destination": "/proc",
587-
"type": "proc"
588-
},
586+
"destination": "/proc",
587+
"type": "proc"
588+
},
589589
{
590-
"destination": "/sys",
591-
"type": "bind",
592-
"source": "/sys",
593-
"options": [
590+
"destination": "/sys",
591+
"type": "bind",
592+
"source": "/sys",
593+
"options": [
594594
"rprivate",
595595
"nosuid",
596596
"noexec",
597597
"nodev",
598598
"ro",
599599
"rbind"
600-
]
601-
},
600+
]
601+
},
602602
{
603603
"destination": "/sys/fs/cgroup",
604604
"type": "cgroup",
605605
"source": "cgroup",
606606
"options": [
607-
"rprivate",
607+
"rprivate",
608608
"nosuid",
609609
"noexec",
610610
"nodev",
@@ -703,6 +703,81 @@ 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 src_nofollow in [True, False]:
723+
conf = base_config()
724+
add_all_namespaces(conf)
725+
if src_nofollow:
726+
options = ["bind", "dest-nofollow", "src-nofollow"]
727+
conf['process']['args'] = ['/init', 'readlink', '/target']
728+
expected = target_content
729+
else:
730+
options = ["bind", "dest-nofollow"]
731+
conf['process']['args'] = ['/init', 'cat', '/target']
732+
expected = file_target_content
733+
734+
mount_opt = {"destination": "/target", "type": "bind", "source": symlink, "options": options}
735+
conf['mounts'].append(mount_opt)
736+
737+
try:
738+
out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs)
739+
sys.stderr.write("got output %s\n" % out)
740+
if expected not in out:
741+
return -1
742+
except Exception as e:
743+
sys.stderr.write("error %s\n" % e)
744+
return -1
745+
746+
return 0
747+
748+
def test_bind_mount_file_nofollow():
749+
root = get_tests_root()
750+
target = os.path.join(root, "a-file")
751+
target_content = "content-of-file"
752+
753+
with open(target, "w+") as f:
754+
f.write(target_content)
755+
756+
def prepare_rootfs(rootfs):
757+
path = os.path.join(rootfs, "symlink")
758+
os.symlink("point-to-nowhere", path)
759+
760+
for src_nofollow in [True, False]:
761+
conf = base_config()
762+
conf['process']['args'] = ['/init', 'cat', '/symlink']
763+
add_all_namespaces(conf)
764+
if src_nofollow:
765+
options = ["bind", "dest-nofollow", "src-nofollow"]
766+
else:
767+
options = ["bind", "dest-nofollow"]
768+
mount_opt = {"destination": "/symlink", "type": "bind", "source": target, "options": options}
769+
conf['mounts'].append(mount_opt)
770+
771+
try:
772+
out, _ = run_and_get_output(conf, hide_stderr=True,callback_prepare_rootfs=prepare_rootfs)
773+
sys.stderr.write("got output %s\n" % out)
774+
if target_content in out:
775+
return 0
776+
except Exception as e:
777+
sys.stderr.write("error %s\n" % e)
778+
pass
779+
return -1
780+
706781
all_tests = {
707782
"mount-ro" : test_mount_ro,
708783
"mount-rro" : test_mount_rro,
@@ -732,6 +807,8 @@ def test_mount_help():
732807
"mount-ro-cgroup": test_ro_cgroup,
733808
"mount-cgroup-without-netns": test_cgroup_mount_without_netns,
734809
"mount-copy-symlink": test_copy_symlink,
810+
"mount-bind-mount-symlink-nofollow": test_bind_mount_symlink_nofollow,
811+
"mount-bind-mount-file-nofollow": test_bind_mount_file_nofollow,
735812
"mount-tmpfs-permissions": test_mount_tmpfs_permissions,
736813
"mount-add-remove-mounts": test_add_remove_mounts,
737814
"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)