diff --git a/kmod/src/ioctl.c b/kmod/src/ioctl.c index 1ae9da1f..3bc9c546 100644 --- a/kmod/src/ioctl.c +++ b/kmod/src/ioctl.c @@ -1398,6 +1398,110 @@ out: return ret ?: nr; } +/* + * Copy entries that point to an inode to the user's buffer. We copy to + * userspace from copies of the entries that are acquired under a lock + * so that we don't fault while holding cluster locks. It also gives us + * a chance to limit the amount of work under each lock hold. + */ +static long scoutfs_ioc_get_referring_entries(struct file *file, unsigned long arg) +{ + struct super_block *sb = file_inode(file)->i_sb; + struct scoutfs_ioctl_get_referring_entries gre; + struct scoutfs_link_backref_entry *bref = NULL; + struct scoutfs_link_backref_entry *bref_tmp; + struct scoutfs_ioctl_dirent __user *uent; + struct scoutfs_ioctl_dirent ent; + LIST_HEAD(list); + u64 copied; + int name_len; + int bytes; + long nr; + int ret; + + if (!capable(CAP_DAC_READ_SEARCH)) + return -EPERM; + + if (copy_from_user(&gre, (void __user *)arg, sizeof(gre))) + return -EFAULT; + + uent = (void __user *)(unsigned long)gre.entries_ptr; + copied = 0; + nr = 0; + + /* use entry as cursor between calls */ + ent.dir_ino = gre.dir_ino; + ent.dir_pos = gre.dir_pos; + + for (;;) { + ret = scoutfs_dir_add_next_linkrefs(sb, gre.ino, ent.dir_ino, ent.dir_pos, 1024, + &list); + if (ret < 0) { + if (ret == -ENOENT) + ret = 0; + goto out; + } + + /* _add_next adds each entry to the head, _reverse for key order */ + list_for_each_entry_safe_reverse(bref, bref_tmp, &list, head) { + list_del_init(&bref->head); + + name_len = bref->name_len; + bytes = ALIGN(offsetof(struct scoutfs_ioctl_dirent, name[name_len + 1]), + 16); + if (copied + bytes > gre.entries_bytes) { + ret = -EINVAL; + goto out; + } + + ent.dir_ino = bref->dir_ino; + ent.dir_pos = bref->dir_pos; + ent.ino = gre.ino; + ent.entry_bytes = bytes; + ent.flags = bref->last ? SCOUTFS_IOCTL_DIRENT_FLAG_LAST : 0; + ent.d_type = bref->d_type; + ent.name_len = name_len; + + if (copy_to_user(uent, &ent, sizeof(struct scoutfs_ioctl_dirent)) || + copy_to_user(&uent->name[0], bref->dent.name, name_len) || + put_user('\0', &uent->name[name_len])) { + ret = -EFAULT; + goto out; + } + + kfree(bref); + bref = NULL; + + uent = (void __user *)uent + bytes; + copied += bytes; + nr++; + + if (nr == LONG_MAX || (ent.flags & SCOUTFS_IOCTL_DIRENT_FLAG_LAST)) { + ret = 0; + goto out; + } + } + + /* advance cursor pos from last copied entry */ + if (++ent.dir_pos == 0) { + if (++ent.dir_ino == 0) { + ret = 0; + goto out; + } + } + } + + ret = 0; +out: + kfree(bref); + list_for_each_entry_safe(bref, bref_tmp, &list, head) { + list_del_init(&bref->head); + kfree(bref); + } + + return nr ?: ret; +} + long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { @@ -1433,6 +1537,8 @@ long scoutfs_ioctl(struct file *file, unsigned int cmd, unsigned long arg) return scoutfs_ioc_read_xattr_totals(file, arg); case SCOUTFS_IOC_GET_ALLOCATED_INOS: return scoutfs_ioc_get_allocated_inos(file, arg); + case SCOUTFS_IOC_GET_REFERRING_ENTRIES: + return scoutfs_ioc_get_referring_entries(file, arg); } return -ENOTTY; diff --git a/kmod/src/ioctl.h b/kmod/src/ioctl.h index 459ee7fb..7be22dbb 100644 --- a/kmod/src/ioctl.h +++ b/kmod/src/ioctl.h @@ -559,4 +559,118 @@ struct scoutfs_ioctl_get_allocated_inos { #define SCOUTFS_IOC_GET_ALLOCATED_INOS \ _IOW(SCOUTFS_IOCTL_MAGIC, 16, struct scoutfs_ioctl_get_allocated_inos) +/* + * Get directory entries that refer to a specific inode. + * + * @ino: The target ino that we're finding referring entries to. + * Constant across all the calls that make up an iteration over all the + * inode's entries. + * + * @dir_ino: The inode number of a directory containing the entry to our + * inode to search from. If this parent directory contains no more + * entries to our inode then we'll search through other parent directory + * inodes in inode order. + * + * @dir_pos: The position in the dir_ino parent directory of the entry + * to our inode to search from. If there is no entry at this position + * then we'll search through other entry positions in increasing order. + * If we exhaust the parent directory then we'll search through + * additional parent directories in inode order. + * + * @entries_ptr: A pointer to the buffer where found entries will be + * stored. The pointer must be aligned to 16 bytes. + * + * @entries_bytes: The size of the buffer that will contain entries. + * + * To start iterating set the desired target ino, dir_ino to 0, dir_pos + * to 0, and set result_ptr and _bytes to a sufficiently large buffer. + * Each entry struct that's stored in the buffer adds some overhead so a + * large multiple of the largest possible name is a reasonable choice. + * (A few multiples of PATH_MAX perhaps.) + * + * Each call returns the total number of entries that were stored in the + * entries buffer. Zero is returned when the search was successful and + * no referring entries were found. The entries can be iterated over by + * advancing each starting struct offset by the total number of bytes in + * each entry. If the _LAST flag is set on an entry then there were no + * more entries referring to the inode at the time of the call and + * iteration can be stopped. + * + * To resume iteration set the next call's starting dir_ino and dir_pos + * to one past the last entry seen. Increment the last entry's dir_pos, + * and if it wrapped to 0, increment its dir_ino. + * + * This does not check that the caller has permission to read the + * entries found in each containing directory. It requires + * CAP_DAC_READ_SEARCH which bypasses path traversal permissions + * checking. + * + * Entries returned by a single call can reflect any combination of + * racing creation and removal of entries. Each entry existed at the + * time it was read though it may have changed in the time it took to + * return from the call. The set of entries returned may no longer + * reflect the current set of entries and may not have existed at the + * same time. + * + * This has no knowledge of the life cycle of the inode. It can return + * 0 when there are no referring entries because either the target inode + * doesn't exist, it is in the process of being deleted, or because it + * is still open while being unlinked. + * + * On success this returns the number of entries filled in the buffer. + * A return of 0 indicates that no entries referred to the inode. + * + * EINVAL is returned when there is a problem with the buffer. Either + * it was not aligned or it was not large enough for the first entry. + * + * Many other errnos indicate hard failure to find the next entry. + */ +struct scoutfs_ioctl_get_referring_entries { + __u64 ino; + __u64 dir_ino; + __u64 dir_pos; + __u64 entries_ptr; + __u64 entries_bytes; +}; + +/* + * @dir_ino: The inode of the directory containing the entry. + * + * @dir_pos: The readdir f_pos position of the entry within the + * directory. + * + * @ino: The inode number of the target of the entry. + * + * @flags: Flags associated with this entry. + * + * @d_type: Inode type as specified with DT_ enum values in readdir(3). + * + * @entry_bytes: The total bytes taken by the entry in memory, including + * the name and any alignment padding. The start of a following entry + * will be found after this number of bytes. + * + * @name_len: The number of bytes in the name not including the trailing + * null, ala strlen(3). + * + * @name: The null terminated name of the referring entry. In the + * struct definition this array is sized to naturally align the struct. + * That number of padded bytes are not necessarily found in the buffer + * returned by _get_referring_entries; + */ +struct scoutfs_ioctl_dirent { + __u64 dir_ino; + __u64 dir_pos; + __u64 ino; + __u16 entry_bytes; + __u8 flags; + __u8 d_type; + __u8 name_len; + __u8 name[3]; +}; + +#define SCOUTFS_IOCTL_DIRENT_FLAG_LAST (1 << 0) + +#define SCOUTFS_IOC_GET_REFERRING_ENTRIES \ + _IOW(SCOUTFS_IOCTL_MAGIC, 17, struct scoutfs_ioctl_get_referring_entries) + #endif