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 <b.sachdev1904@gmail.com>
This commit is contained in:
Bhavik Sachdev 2025-06-11 01:25:47 +05:30
parent d7b3d870ac
commit ebfff96a10
5 changed files with 303 additions and 0 deletions

View file

@ -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();

View file

@ -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;

View file

@ -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 */

View file

@ -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();

View file

@ -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;