From ebfff96a10254d54b86cb855baba218ed985df92 Mon Sep 17 00:00:00 2001 From: Bhavik Sachdev Date: Wed, 11 Jun 2025 01:25:47 +0530 Subject: [PATCH] kerndat: check support for statmount, STATMOUNT_BY_FD statmount() can be used to query for mount using either a unique mnt_id or a file descriptor with STATMOUNT_BY_FD. We want to use statmount() with STATMOUNT_BY_FD to add support for c/r of files on unmounted bind mounts (mounts unmounted with MNT_DETACH). Signed-off-by: Bhavik Sachdev --- criu/cr-check.c | 12 ++++ criu/include/kerndat.h | 3 + criu/include/mount-v2.h | 142 ++++++++++++++++++++++++++++++++++++++++ criu/kerndat.c | 116 ++++++++++++++++++++++++++++++++ criu/mount-v2.c | 30 +++++++++ 5 files changed, 303 insertions(+) diff --git a/criu/cr-check.c b/criu/cr-check.c index d7aa43076..b5c32bb7d 100644 --- a/criu/cr-check.c +++ b/criu/cr-check.c @@ -1400,6 +1400,17 @@ static int check_timer_cr_ids(void) return 0; } +static int check_statmount_by_fd(void) +{ + if (!kdat.has_statmount_by_fd) { + pr_warn("statmount syscall with STATMOUNT_BY_FD is unavailable," + " files on unmounted bind mounts will not be supported\n"); + return -1; + } + + return 0; +} + /* musl doesn't have a statx wrapper... */ struct staty { __u32 stx_dev_major; @@ -1721,6 +1732,7 @@ int cr_check(void) ret |= check_overlayfs_maps(); ret |= check_timer_cr_ids(); ret |= check_pagemap_scan_guard_pages(); + ret |= check_statmount_by_fd(); if (kdat.lsm == LSMTYPE__APPARMOR) ret |= check_apparmor_stacking(); diff --git a/criu/include/kerndat.h b/criu/include/kerndat.h index 27bdfe557..9c964c136 100644 --- a/criu/include/kerndat.h +++ b/criu/include/kerndat.h @@ -93,6 +93,9 @@ struct kerndat_s { bool has_madv_guard; bool has_pagemap_scan_guard_pages; bool has_binfmt_misc_sandboxing; + bool has_statmount; + u64 statmount_supported_mask; + bool has_statmount_by_fd; }; extern struct kerndat_s kdat; diff --git a/criu/include/mount-v2.h b/criu/include/mount-v2.h index 096f08f3b..8419bebe0 100644 --- a/criu/include/mount-v2.h +++ b/criu/include/mount-v2.h @@ -57,7 +57,149 @@ static inline long sys_openat2(int dirfd, const char *pathname, struct open_how return syscall(__NR_openat2, dirfd, pathname, how, size); } +/* flags and structs related to statmount */ +#ifndef STATMOUNT_SB_BASIC +#define STATMOUNT_SB_BASIC 0x00000001U /* Want/got sb_... */ +#endif + +#ifndef STATMOUNT_MNT_BASIC +#define STATMOUNT_MNT_BASIC 0x00000002U /* Want/got mnt_... */ +#endif + +#ifndef STATMOUNT_PROPAGATE_FROM +#define STATMOUNT_PROPAGATE_FROM 0x00000004U /* Want/got propagate_from */ +#endif + +#ifndef STATMOUNT_MNT_ROOT +#define STATMOUNT_MNT_ROOT 0x00000008U /* Want/got mnt_root */ +#endif + +#ifndef STATMOUNT_MNT_POINT +#define STATMOUNT_MNT_POINT 0x00000010U /* Want/got mnt_point */ +#endif + +#ifndef STATMOUNT_FS_TYPE +#define STATMOUNT_FS_TYPE 0x00000020U /* Want/got fs_type */ +#endif + +#ifndef STATMOUNT_MNT_NS_ID +#define STATMOUNT_MNT_NS_ID 0x00000040U /* Want/got mnt_ns_id */ +#endif + +#ifndef STATMOUNT_MNT_OPTS +#define STATMOUNT_MNT_OPTS 0x00000080U /* Want/got mnt_opts */ +#endif + +#ifndef STATMOUNT_FS_SUBTYPE +#define STATMOUNT_FS_SUBTYPE 0x00000100U /* Want/got fs_subtype */ +#endif + +#ifndef STATMOUNT_SB_SOURCE +#define STATMOUNT_SB_SOURCE 0x00000200U /* Want/got sb_source */ +#endif + +#ifndef STATMOUNT_OPT_ARRAY +#define STATMOUNT_OPT_ARRAY 0x00000400U /* Want/got opt_... */ +#endif + +#ifndef STATMOUNT_OPT_SEC_ARRAY +#define STATMOUNT_OPT_SEC_ARRAY 0x00000800U /* Want/got opt_sec... */ +#endif + +#ifndef STATMOUNT_SUPPORTED_MASK +#define STATMOUNT_SUPPORTED_MASK 0x00001000U /* Want/got supported mask flags */ +#endif + +#ifndef STATMOUNT_MNT_UIDMAP +#define STATMOUNT_MNT_UIDMAP 0x00002000U /* Want/got uidmap... */ +#endif + +#ifndef STATMOUNT_MNT_GIDMAP +#define STATMOUNT_MNT_GIDMAP 0x00004000U /* Want/got gidmap... */ +#endif + +#ifndef STATMOUNT_BY_FD +#define STATMOUNT_BY_FD 0x0000001U /* want mountinfo for given fd */ +#endif + +#ifndef MNT_ID_REQ_SIZE_VER1 +#define MNT_ID_REQ_SIZE_VER1 32 /* sizeof second published struct */ +#endif + +/* + * Structure for passing mount ID and miscellaneous parameters to statmount(2) + * and listmount(2). + * + * For statmount(2) @param represents the request mask. + * For listmount(2) @param represents the last listed mount id (or zero). + */ +struct cr_mnt_id_req { + __u32 size; + union { + __u32 mnt_ns_fd; + __u32 mnt_fd; + }; + __u64 mnt_id; + __u64 param; + __u64 mnt_ns_id; +}; + +/* + * Structure for getting mount/superblock/filesystem info with statmount(2). + * + * The interface is similar to statx(2): individual fields or groups can be + * selected with the @mask argument of statmount(). Kernel will set the @mask + * field according to the supported fields. + * + * If string fields are selected, then the caller needs to pass a buffer that + * has space after the fixed part of the structure. Nul terminated strings are + * copied there and offsets relative to @str are stored in the relevant fields. + * If the buffer is too small, then EOVERFLOW is returned. The actually used + * size is returned in @size. + */ +struct cr_statmount { + __u32 size; /* Total size, including strings */ + __u32 mnt_opts; /* [str] Options (comma separated, escaped) */ + __u64 mask; /* What results were written */ + __u32 sb_dev_major; /* Device ID */ + __u32 sb_dev_minor; + __u64 sb_magic; /* ..._SUPER_MAGIC */ + __u32 sb_flags; /* SB_{RDONLY,SYNCHRONOUS,DIRSYNC,LAZYTIME} */ + __u32 fs_type; /* [str] Filesystem type */ + __u64 mnt_id; /* Unique ID of mount */ + __u64 mnt_parent_id; /* Unique ID of parent (for root == mnt_id) */ + __u32 mnt_id_old; /* Reused IDs used in proc/.../mountinfo */ + __u32 mnt_parent_id_old; + __u64 mnt_attr; /* MOUNT_ATTR_... */ + __u64 mnt_propagation; /* MS_{SHARED,SLAVE,PRIVATE,UNBINDABLE} */ + __u64 mnt_peer_group; /* ID of shared peer group */ + __u64 mnt_master; /* Mount receives propagation from this ID */ + __u64 propagate_from; /* Propagation from in current namespace */ + __u32 mnt_root; /* [str] Root of mount relative to root of fs */ + __u32 mnt_point; /* [str] Mountpoint relative to current root */ + __u64 mnt_ns_id; /* ID of the mount namespace */ + __u32 fs_subtype; /* [str] Subtype of fs_type (if any) */ + __u32 sb_source; /* [str] Source string of the mount */ + __u32 opt_num; /* Number of fs options */ + __u32 opt_array; /* [str] Array of nul terminated fs options */ + __u32 opt_sec_num; /* Number of security options */ + __u32 opt_sec_array; /* [str] Array of nul terminated security options */ + __u64 supported_mask; /* Mask flags that this kernel supports */ + __u32 mnt_uidmap_num; /* Number of uid mappings */ + __u32 mnt_uidmap; /* [str] Array of uid mappings (as seen from callers namespace) */ + __u32 mnt_gidmap_num; /* Number of gid mappings */ + __u32 mnt_gidmap; /* [str] Array of gid mappings (as seen from callers namespace) */ + __u64 __spare2[43]; + char str[]; /* Variable size part containing strings */ +}; + +static inline int sys_statmount(struct cr_mnt_id_req *req, struct cr_statmount *smbuf, size_t bufsize, unsigned long flags) +{ + return syscall(__NR_statmount, req, smbuf, bufsize, flags); +} + extern int check_mount_v2(void); +extern struct cr_statmount *do_statmount(struct cr_mnt_id_req *req, unsigned int flags); struct sharing_group { /* This pair identifies the group */ diff --git a/criu/kerndat.c b/criu/kerndat.c index 1e4863190..136506d01 100644 --- a/criu/kerndat.c +++ b/criu/kerndat.c @@ -1736,6 +1736,114 @@ static int kerndat_has_timer_cr_ids(void) return 0; } +static int kerndat_has_statmount(void) +{ + /* + * passing in a invalid mount, the first possible + * valid mount is (1ULL << 31) + 1. So this call + * should always either return EINVAL or ENOENT + * if statmount is supported. + */ + struct cr_mnt_id_req req = { + .size = MNT_ID_REQ_SIZE_VER1, + .mnt_id = 1, + .param = STATMOUNT_MNT_BASIC, + }; + struct cr_statmount *statmnt = do_statmount(&req, 0); + if (statmnt) { + pr_err("statmount() call unexpectedly succeeded during kerndat check\n"); + free(statmnt); + return -1; + } + + switch (errno) { + case ENOSYS: + kdat.has_statmount = false; + kdat.statmount_supported_mask = 0; + pr_info("statmount() isn't supported\n"); + return 0; + case EINVAL: + case ENOENT: + case EPERM: + /* + * Treat EPERM as potentially supported so that we can still probe + * STATMOUNT_BY_FD, whose fd-based access checks differ from mount-ID + * lookup. If seccomp or something else blocks statmount entirely + * that probe fails and marks statmount() as not supported. + */ + kdat.has_statmount = true; + /* + * these are the flags that we can guarantee that statmount supports + * as they were introduced alongside the syscall. + * This gets updated to the actual mask supported below + * in kerndat_has_statmount_by_fd(). + */ + kdat.statmount_supported_mask = STATMOUNT_SB_BASIC | + STATMOUNT_MNT_BASIC | + STATMOUNT_PROPAGATE_FROM | + STATMOUNT_MNT_ROOT | + STATMOUNT_MNT_POINT | + STATMOUNT_FS_TYPE; + return 0; + default: + pr_perror("statmount() call returned unexpected errno, assuming statmount() is not supported"); + kdat.has_statmount = false; + kdat.statmount_supported_mask = 0; + return 0; + } +} + +static int kerndat_has_statmount_by_fd(void) +{ + struct cr_statmount *statmnt; + int tempfd, ret = 0; + struct cr_mnt_id_req req = { + .size = MNT_ID_REQ_SIZE_VER1, + .param = STATMOUNT_SUPPORTED_MASK, + }; + char tempfile[PATH_MAX] = "/tmp/.criu.kerndat.XXXXXX"; + + if (!kdat.has_statmount) { + kdat.has_statmount_by_fd = false; + return 0; + } + + tempfd = mkstemp(tempfile); + if (tempfd < 0) { + pr_perror("could not create temporary file"); + return -1; + } + + req.mnt_fd = tempfd; + statmnt = do_statmount(&req, STATMOUNT_BY_FD); + if (statmnt) { + kdat.has_statmount_by_fd = true; + if (statmnt->mask & STATMOUNT_SUPPORTED_MASK) + kdat.statmount_supported_mask = statmnt->supported_mask; + free(statmnt); + goto out; + } + + switch (errno) { + case ENOMEM: + case EFAULT: + pr_perror("failed to check for STATMOUNT_BY_FD support"); + ret = -1; + goto out; + case EPERM: + kdat.has_statmount = false; + kdat.statmount_supported_mask = 0; + /* fallthrough */ + default: + pr_info("STATMOUNT_BY_FD flag isn't supported\n"); + kdat.has_statmount_by_fd = false; + } +out: + close(tempfd); + unlink(tempfile); + return ret; +} + static int kerndat_has_madv_guard(void) { void *map; @@ -2130,6 +2238,14 @@ int kerndat_init(void) pr_err("kerndat_has_binfmt_misc_sandboxing has failed when initializing kerndat.\n"); ret = -1; } + if (!ret && kerndat_has_statmount()) { + pr_err("kerndat_has_statmount failed when initializing kerndat.\n"); + ret = -1; + } + if (!ret && kerndat_has_statmount_by_fd()) { + pr_err("kerndat_has_statmount_by_fd failed when initializing kerndat.\n"); + ret = -1; + } kerndat_lsm(); kerndat_mmap_min_addr(); diff --git a/criu/mount-v2.c b/criu/mount-v2.c index 321f4c009..a8ed87f69 100644 --- a/criu/mount-v2.c +++ b/criu/mount-v2.c @@ -45,6 +45,36 @@ int check_mount_v2(void) return 0; } +struct cr_statmount *do_statmount(struct cr_mnt_id_req *req, unsigned int flags) +{ + size_t bufsize = 1 << 15; + struct cr_statmount *stmnt = NULL, *tmp = NULL; + int ret; + + for (;;) { + tmp = xrealloc(stmnt, bufsize); + if (!tmp) + goto out; + + stmnt = tmp; + ret = sys_statmount(req, stmnt, bufsize, flags); + if (!ret) + return stmnt; + + if (errno != EOVERFLOW) + goto out; + + bufsize <<= 1; + if (bufsize >= UINT_MAX / 2) + goto out; + } + +out: + free(stmnt); + return NULL; +} + + static struct sharing_group *get_sharing_group(int shared_id, int master_id) { struct sharing_group *sg;