1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2 /*
3 * This file is part of libmount from util-linux project.
4 *
5 * Copyright (C) 2022 Karel Zak <kzak@redhat.com>
6 *
7 * libmount is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU Lesser General Public License as published by
9 * the Free Software Foundation; either version 2.1 of the License, or
10 * (at your option) any later version.
11 *
12 *
13 * This is X-mount.subdir= implementation. The code uses global hookset data
14 * rather than per-callback (hook) data.
15 *
16 * Please, see the comment in libmount/src/hooks.c to understand how hooks work.
17 */
18 #include <sched.h>
19
20 #include "mountP.h"
21 #include "fileutils.h"
22 #include "mount-api-utils.h"
23
24 struct hookset_data {
25 char *subdir;
26 char *org_target;
27 int old_ns_fd;
28 int new_ns_fd;
29 unsigned int tmp_umounted : 1;
30 };
31
32 static int tmptgt_cleanup(struct hookset_data *);
33
34 static void free_hookset_data( struct libmnt_context *cxt,
35 const struct libmnt_hookset *hs)
36 {
37 struct hookset_data *hsd = mnt_context_get_hookset_data(cxt, hs);
38
39 if (!hsd)
40 return;
41 if (hsd->old_ns_fd >= 0)
42 tmptgt_cleanup(hsd);
43
44 free(hsd->org_target);
45 free(hsd->subdir);
46 free(hsd);
47
48 mnt_context_set_hookset_data(cxt, hs, NULL);
49 }
50
51 /* global data, used by all callbacks */
52 static struct hookset_data *new_hookset_data(
53 struct libmnt_context *cxt,
54 const struct libmnt_hookset *hs)
55 {
56 struct hookset_data *hsd = calloc(1, sizeof(struct hookset_data));
57
58 if (hsd && mnt_context_set_hookset_data(cxt, hs, hsd) != 0) {
59 /* probably ENOMEM problem */
60 free(hsd);
61 hsd = NULL;
62 }
63 return hsd;
64 }
65
66 /* de-initiallize this module */
67 static int hookset_deinit(struct libmnt_context *cxt, const struct libmnt_hookset *hs)
68 {
69 DBG(HOOK, ul_debugobj(hs, "deinit '%s'", hs->name));
70
71 /* remove all our hooks */
72 while (mnt_context_remove_hook(cxt, hs, 0, NULL) == 0);
73
74 /* free and remove global hookset data */
75 free_hookset_data(cxt, hs);
76
77 return 0;
78 }
79
80 /*
81 * Initialize MNT_PATH_TMPTGT; mkdir, create a new namespace and
82 * mark (bind mount) the directory as private.
83 */
84 static int tmptgt_unshare(struct hookset_data *hsd)
85 {
86 #ifdef USE_LIBMOUNT_SUPPORT_NAMESPACES
87 int rc = 0;
88
89 hsd->old_ns_fd = hsd->new_ns_fd = -1;
90
91 /* create directory */
92 rc = ul_mkdir_p(MNT_PATH_TMPTGT, S_IRWXU);
93 if (rc)
94 goto fail;
95
96 /* remember the current namespace */
97 hsd->old_ns_fd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
98 if (hsd->old_ns_fd < 0)
99 goto fail;
100
101 /* create new namespace */
102 if (unshare(CLONE_NEWNS) != 0)
103 goto fail;
104
105 /* try to set top-level directory as private, this is possible if
106 * MNT_RUNTIME_TOPDIR (/run) is a separated filesystem. */
107 if (mount("none", MNT_RUNTIME_TOPDIR, NULL, MS_PRIVATE, NULL) != 0) {
108
109 /* failed; create a mountpoint from MNT_PATH_TMPTGT */
110 if (mount(MNT_PATH_TMPTGT, MNT_PATH_TMPTGT, "none", MS_BIND, NULL) != 0)
111 goto fail;
112 if (mount("none", MNT_PATH_TMPTGT, NULL, MS_PRIVATE, NULL) != 0)
113 goto fail;
114 }
115
116 /* remember the new namespace */
117 hsd->new_ns_fd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
118 if (hsd->new_ns_fd < 0)
119 goto fail;
120
121 DBG(UTILS, ul_debug(MNT_PATH_TMPTGT " unshared"));
122 return 0;
123 fail:
124 if (rc == 0)
125 rc = errno ? -errno : -EINVAL;
126
127 tmptgt_cleanup(hsd);
128 DBG(UTILS, ul_debug(MNT_PATH_TMPTGT " unshare failed"));
129 return rc;
130 #else
131 return -ENOSYS;
132 #endif
133 }
134
135 /*
136 * Clean up MNT_PATH_TMPTGT; umount and switch back to old namespace
137 */
138 static int tmptgt_cleanup(struct hookset_data *hsd)
139 {
140 #ifdef USE_LIBMOUNT_SUPPORT_NAMESPACES
141 if (!hsd->tmp_umounted) {
142 umount(MNT_PATH_TMPTGT);
143 hsd->tmp_umounted = 1;
144 }
145
146 if (hsd->new_ns_fd >= 0)
147 close(hsd->new_ns_fd);
148
149 if (hsd->old_ns_fd >= 0) {
150 setns(hsd->old_ns_fd, CLONE_NEWNS);
151 close(hsd->old_ns_fd);
152 }
153
154 hsd->new_ns_fd = hsd->old_ns_fd = -1;
155 DBG(UTILS, ul_debug(MNT_PATH_TMPTGT " cleanup done"));
156 return 0;
157 #else
158 return -ENOSYS;
159 #endif
160 }
161
162 /*
163 * Attach (move) MNT_PATH_TMPTGT/subdir to the parental namespace.
164 */
165 static int do_mount_subdir(
166 struct libmnt_context *cxt,
167 struct hookset_data *hsd,
168 const char *root,
169 const char *target)
170 {
171 int rc = 0;
172 const char *subdir = hsd->subdir;
173
174 #ifdef USE_LIBMOUNT_MOUNTFD_SUPPORT
175 struct libmnt_sysapi *api;
176
177 api = mnt_context_get_sysapi(cxt);
178 if (api) {
179 /* FD based way - unfortunately, it's impossible to open
180 * sub-directory on not-yet attached mount. It means
181 * hook_mount.c attaches FS to temporary directory, and we
182 * clone and move the subdir, and umount the old unshared
183 * temporary tree.
184 *
185 * The old mount(2) way does the same, but by BIND.
186 */
187 int fd;
188
189 DBG(HOOK, ul_debug("attach subdir %s", subdir));
190 fd = open_tree(api->fd_tree, subdir,
191 OPEN_TREE_CLOEXEC | OPEN_TREE_CLONE);
192 set_syscall_status(cxt, "open_tree", fd >= 0);
193 if (fd < 0)
194 rc = -errno;
195
196 if (!rc) {
197 /* Note that the original parental namespace could be
198 * private, in this case, it will not see our final mount,
199 * so we need to move the the orignal namespace.
200 */
201 setns(hsd->old_ns_fd, CLONE_NEWNS);
202
203 rc = move_mount(fd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH);
204 set_syscall_status(cxt, "move_mount", rc == 0);
205 if (rc)
206 rc = -errno;
207
208 /* And move back to our private namespace to cleanup */
209 setns(hsd->new_ns_fd, CLONE_NEWNS);
210 }
211 if (!rc) {
212 close(api->fd_tree);
213 api->fd_tree = fd;
214 }
215 } else
216 #endif
217 {
218 char *src = NULL;
219
220 if (asprintf(&src, "%s/%s", root, subdir) < 0)
221 return -ENOMEM;
222
223 /* Classic mount(2) based way */
224 DBG(HOOK, ul_debug("mount subdir %s to %s", src, target));
225 rc = mount(src, target, NULL, MS_BIND, NULL);
226
227 set_syscall_status(cxt, "mount", rc == 0);
228 if (rc)
229 rc = -errno;
230 free(src);
231 }
232
233 if (!rc) {
234 DBG(HOOK, ul_debug("umount old root %s", root));
235 rc = umount(root);
236 set_syscall_status(cxt, "umount", rc == 0);
237 if (rc)
238 rc = -errno;
239 hsd->tmp_umounted = 1;
240
241 }
242
243 return rc;
244 }
245
246
247 static int hook_mount_post(
248 struct libmnt_context *cxt,
249 const struct libmnt_hookset *hs,
250 void *data __attribute__((__unused__)))
251 {
252 struct hookset_data *hsd;
253 int rc = 0;
254
255 hsd = mnt_context_get_hookset_data(cxt, hs);
256 if (!hsd || !hsd->subdir)
257 return 0;
258
259 /* reset to the original mountpoint */
260 mnt_fs_set_target(cxt->fs, hsd->org_target);
261
262 /* bind subdir to the real target, umount temporary target */
263 rc = do_mount_subdir(cxt, hsd,
264 MNT_PATH_TMPTGT,
265 mnt_fs_get_target(cxt->fs));
266 if (rc)
267 return rc;
268
269 tmptgt_cleanup(hsd);
270
271 return rc;
272 }
273
274 static int hook_mount_pre(
275 struct libmnt_context *cxt,
276 const struct libmnt_hookset *hs,
277 void *data __attribute__((__unused__)))
278 {
279 struct hookset_data *hsd;
280 int rc = 0;
281
282 hsd = mnt_context_get_hookset_data(cxt, hs);
283 if (!hsd)
284 return 0;
285
286 /* create unhared temporary target */
287 hsd->org_target = strdup(mnt_fs_get_target(cxt->fs));
288 if (!hsd->org_target)
289 rc = -ENOMEM;
290 if (!rc)
291 rc = tmptgt_unshare(hsd);
292 if (!rc)
293 mnt_fs_set_target(cxt->fs, MNT_PATH_TMPTGT);
294 if (!rc)
295 rc = mnt_context_append_hook(cxt, hs,
296 MNT_STAGE_MOUNT_POST,
297 NULL, hook_mount_post);
298
299 DBG(HOOK, ul_debugobj(hs, "unshared tmp target %s [rc=%d]",
300 MNT_PATH_TMPTGT, rc));
301 return rc;
302 }
303
304
305
306 static int is_subdir_required(struct libmnt_context *cxt, int *rc, char **subdir)
307 {
308 struct libmnt_optlist *ol;
309 struct libmnt_opt *opt;
310 const char *dir = NULL;
311
312 assert(cxt);
313 assert(rc);
314
315 *rc = 0;
316
317 ol = mnt_context_get_optlist(cxt);
318 if (!ol)
319 return -ENOMEM;
320
321 opt = mnt_optlist_get_named(ol, "X-mount.subdir", cxt->map_userspace);
322 if (!opt)
323 return 0;
324
325 dir = mnt_opt_get_value(opt);
326
327 if (dir && *dir == '"')
328 dir++;
329
330 if (!dir || !*dir) {
331 DBG(HOOK, ul_debug("failed to parse X-mount.subdir '%s'", dir));
332 *rc = -MNT_ERR_MOUNTOPT;
333 } else {
334 *subdir = strdup(dir);
335 if (!*subdir)
336 *rc = -ENOMEM;
337 }
338
339 return *rc == 0;
340 }
341
342 /* this is the initial callback used to check mount options and define next
343 * actions if necessary */
344 static int hook_prepare_target(
345 struct libmnt_context *cxt,
346 const struct libmnt_hookset *hs,
347 void *data __attribute__((__unused__)))
348 {
349 const char *tgt;
350 char *subdir = NULL;
351 int rc = 0;
352
353 assert(cxt);
354
355 tgt = mnt_fs_get_target(cxt->fs);
356 if (!tgt)
357 return 0;
358
359 if (cxt->action == MNT_ACT_MOUNT
360 && is_subdir_required(cxt, &rc, &subdir)) {
361
362 /* create a global data */
363 struct hookset_data *hsd = new_hookset_data(cxt, hs);
364
365 if (!hsd) {
366 free(subdir);
367 return -ENOMEM;
368 }
369 hsd->subdir = subdir;
370
371 DBG(HOOK, ul_debugobj(hs, "subdir %s wanted", subdir));
372
373 rc = mnt_context_append_hook(cxt, hs,
374 MNT_STAGE_MOUNT_PRE,
375 NULL, hook_mount_pre);
376 }
377
378 return rc;
379 }
380
381 const struct libmnt_hookset hookset_subdir =
382 {
383 .name = "__subdir",
384
385 .firststage = MNT_STAGE_PREP_TARGET,
386 .firstcall = hook_prepare_target,
387
388 .deinit = hookset_deinit
389 };