
Syzbot recently found a number of issues related to incremental-fs (see bug numbers below). All have to do with the fact that incr-fs allows mounts of the same source and target multiple times. This is a design decision and the user space component "Data Loader" expects this to work for app re-install use case. The mounting depth needs to be controlled, however, and only allowed to be two levels deep. In case of more than two mount attempts the driver needs to return an error. In case of the issues listed below the common pattern is that the reproducer calls: mount("./file0", "./file0", "incremental-fs", 0, NULL) many times and then invokes a file operation like chmod, setxattr, or open on the ./file0. This causes a recursive call for all the mounted instances, which eventually causes a stack overflow and a kernel crash: BUG: stack guard page was hit at ffffc90000c0fff8 kernel stack overflow (double-fault): 0000 [#1] PREEMPT SMP KASAN This change also cleans up the mount error path to properly clean allocated resources and call deactivate_locked_super(), which causes the incfs_kill_sb() to be called, where the sb is freed. Bug: 211066171 Bug: 213140206 Bug: 213215835 Bug: 211914587 Bug: 211213635 Bug: 213137376 Bug: 211161296 Signed-off-by: Tadeusz Struk <tadeusz.struk@linaro.org> Change-Id: I08d9b545a2715423296bf4beb67bdbbed78d1be1
1944 lines
47 KiB
C
1944 lines
47 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright 2018 Google LLC
|
|
*/
|
|
|
|
#include <linux/blkdev.h>
|
|
#include <linux/compat.h>
|
|
#include <linux/file.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/fs_stack.h>
|
|
#include <linux/fsnotify.h>
|
|
#include <linux/fsverity.h>
|
|
#include <linux/mmap_lock.h>
|
|
#include <linux/namei.h>
|
|
#include <linux/parser.h>
|
|
#include <linux/seq_file.h>
|
|
|
|
#include <uapi/linux/incrementalfs.h>
|
|
|
|
#include "vfs.h"
|
|
|
|
#include "data_mgmt.h"
|
|
#include "format.h"
|
|
#include "internal.h"
|
|
#include "pseudo_files.h"
|
|
#include "sysfs.h"
|
|
#include "verity.h"
|
|
|
|
static int incfs_remount_fs(struct super_block *sb, int *flags, char *data);
|
|
|
|
static int dentry_revalidate(struct dentry *dentry, unsigned int flags);
|
|
static void dentry_release(struct dentry *d);
|
|
|
|
static int iterate_incfs_dir(struct file *file, struct dir_context *ctx);
|
|
static struct dentry *dir_lookup(struct inode *dir_inode,
|
|
struct dentry *dentry, unsigned int flags);
|
|
static int dir_mkdir(struct inode *dir, struct dentry *dentry, umode_t mode);
|
|
static int dir_unlink(struct inode *dir, struct dentry *dentry);
|
|
static int dir_link(struct dentry *old_dentry, struct inode *dir,
|
|
struct dentry *new_dentry);
|
|
static int dir_rmdir(struct inode *dir, struct dentry *dentry);
|
|
static int dir_rename(struct inode *old_dir, struct dentry *old_dentry,
|
|
struct inode *new_dir, struct dentry *new_dentry);
|
|
|
|
static int file_open(struct inode *inode, struct file *file);
|
|
static int file_release(struct inode *inode, struct file *file);
|
|
static int read_single_page(struct file *f, struct page *page);
|
|
static long dispatch_ioctl(struct file *f, unsigned int req, unsigned long arg);
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
static long incfs_compat_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg);
|
|
#endif
|
|
|
|
static struct inode *alloc_inode(struct super_block *sb);
|
|
static void free_inode(struct inode *inode);
|
|
static void evict_inode(struct inode *inode);
|
|
|
|
static int incfs_setattr(struct dentry *dentry, struct iattr *ia);
|
|
static int incfs_getattr(const struct path *path,
|
|
struct kstat *stat, u32 request_mask,
|
|
unsigned int query_flags);
|
|
static ssize_t incfs_getxattr(struct dentry *d, const char *name,
|
|
void *value, size_t size);
|
|
static ssize_t incfs_setxattr(struct dentry *d, const char *name,
|
|
const void *value, size_t size, int flags);
|
|
static ssize_t incfs_listxattr(struct dentry *d, char *list, size_t size);
|
|
|
|
static int show_options(struct seq_file *, struct dentry *);
|
|
|
|
static const struct super_operations incfs_super_ops = {
|
|
.statfs = simple_statfs,
|
|
.remount_fs = incfs_remount_fs,
|
|
.alloc_inode = alloc_inode,
|
|
.destroy_inode = free_inode,
|
|
.evict_inode = evict_inode,
|
|
.show_options = show_options
|
|
};
|
|
|
|
static int dir_rename_wrap(struct inode *old_dir, struct dentry *old_dentry,
|
|
struct inode *new_dir, struct dentry *new_dentry,
|
|
unsigned int flags)
|
|
{
|
|
return dir_rename(old_dir, old_dentry, new_dir, new_dentry);
|
|
}
|
|
|
|
static const struct inode_operations incfs_dir_inode_ops = {
|
|
.lookup = dir_lookup,
|
|
.mkdir = dir_mkdir,
|
|
.rename = dir_rename_wrap,
|
|
.unlink = dir_unlink,
|
|
.link = dir_link,
|
|
.rmdir = dir_rmdir,
|
|
.setattr = incfs_setattr,
|
|
};
|
|
|
|
static const struct file_operations incfs_dir_fops = {
|
|
.llseek = generic_file_llseek,
|
|
.read = generic_read_dir,
|
|
.iterate = iterate_incfs_dir,
|
|
.open = file_open,
|
|
.release = file_release,
|
|
};
|
|
|
|
static const struct dentry_operations incfs_dentry_ops = {
|
|
.d_revalidate = dentry_revalidate,
|
|
.d_release = dentry_release
|
|
};
|
|
|
|
static const struct address_space_operations incfs_address_space_ops = {
|
|
.readpage = read_single_page,
|
|
/* .readpages = readpages */
|
|
};
|
|
|
|
static vm_fault_t incfs_fault(struct vm_fault *vmf)
|
|
{
|
|
vmf->flags &= ~FAULT_FLAG_ALLOW_RETRY;
|
|
return filemap_fault(vmf);
|
|
}
|
|
|
|
static const struct vm_operations_struct incfs_file_vm_ops = {
|
|
.fault = incfs_fault,
|
|
.map_pages = filemap_map_pages,
|
|
.page_mkwrite = filemap_page_mkwrite,
|
|
};
|
|
|
|
/* This is used for a general mmap of a disk file */
|
|
|
|
static int incfs_file_mmap(struct file *file, struct vm_area_struct *vma)
|
|
{
|
|
struct address_space *mapping = file->f_mapping;
|
|
|
|
if (!mapping->a_ops->readpage)
|
|
return -ENOEXEC;
|
|
file_accessed(file);
|
|
vma->vm_ops = &incfs_file_vm_ops;
|
|
return 0;
|
|
}
|
|
|
|
const struct file_operations incfs_file_ops = {
|
|
.open = file_open,
|
|
.release = file_release,
|
|
.read_iter = generic_file_read_iter,
|
|
.mmap = incfs_file_mmap,
|
|
.splice_read = generic_file_splice_read,
|
|
.llseek = generic_file_llseek,
|
|
.unlocked_ioctl = dispatch_ioctl,
|
|
#ifdef CONFIG_COMPAT
|
|
.compat_ioctl = incfs_compat_ioctl,
|
|
#endif
|
|
};
|
|
|
|
const struct inode_operations incfs_file_inode_ops = {
|
|
.setattr = incfs_setattr,
|
|
.getattr = incfs_getattr,
|
|
.listxattr = incfs_listxattr
|
|
};
|
|
|
|
static int incfs_handler_getxattr(const struct xattr_handler *xh,
|
|
struct dentry *d, struct inode *inode,
|
|
const char *name, void *buffer, size_t size,
|
|
int flags)
|
|
{
|
|
return incfs_getxattr(d, name, buffer, size);
|
|
}
|
|
|
|
static int incfs_handler_setxattr(const struct xattr_handler *xh,
|
|
struct dentry *d, struct inode *inode,
|
|
const char *name, const void *buffer,
|
|
size_t size, int flags)
|
|
{
|
|
return incfs_setxattr(d, name, buffer, size, flags);
|
|
}
|
|
|
|
static const struct xattr_handler incfs_xattr_handler = {
|
|
.prefix = "", /* AKA all attributes */
|
|
.get = incfs_handler_getxattr,
|
|
.set = incfs_handler_setxattr,
|
|
};
|
|
|
|
static const struct xattr_handler *incfs_xattr_ops[] = {
|
|
&incfs_xattr_handler,
|
|
NULL,
|
|
};
|
|
|
|
struct inode_search {
|
|
unsigned long ino;
|
|
|
|
struct dentry *backing_dentry;
|
|
|
|
size_t size;
|
|
|
|
bool verity;
|
|
};
|
|
|
|
enum parse_parameter {
|
|
Opt_read_timeout,
|
|
Opt_readahead_pages,
|
|
Opt_rlog_pages,
|
|
Opt_rlog_wakeup_cnt,
|
|
Opt_report_uid,
|
|
Opt_sysfs_name,
|
|
Opt_err
|
|
};
|
|
|
|
static const match_table_t option_tokens = {
|
|
{ Opt_read_timeout, "read_timeout_ms=%u" },
|
|
{ Opt_readahead_pages, "readahead=%u" },
|
|
{ Opt_rlog_pages, "rlog_pages=%u" },
|
|
{ Opt_rlog_wakeup_cnt, "rlog_wakeup_cnt=%u" },
|
|
{ Opt_report_uid, "report_uid" },
|
|
{ Opt_sysfs_name, "sysfs_name=%s" },
|
|
{ Opt_err, NULL }
|
|
};
|
|
|
|
static void free_options(struct mount_options *opts)
|
|
{
|
|
kfree(opts->sysfs_name);
|
|
opts->sysfs_name = NULL;
|
|
}
|
|
|
|
static int parse_options(struct mount_options *opts, char *str)
|
|
{
|
|
substring_t args[MAX_OPT_ARGS];
|
|
int value;
|
|
char *position;
|
|
|
|
if (opts == NULL)
|
|
return -EFAULT;
|
|
|
|
*opts = (struct mount_options) {
|
|
.read_timeout_ms = 1000, /* Default: 1s */
|
|
.readahead_pages = 10,
|
|
.read_log_pages = 2,
|
|
.read_log_wakeup_count = 10,
|
|
};
|
|
|
|
if (str == NULL || *str == 0)
|
|
return 0;
|
|
|
|
while ((position = strsep(&str, ",")) != NULL) {
|
|
int token;
|
|
|
|
if (!*position)
|
|
continue;
|
|
|
|
token = match_token(position, option_tokens, args);
|
|
|
|
switch (token) {
|
|
case Opt_read_timeout:
|
|
if (match_int(&args[0], &value))
|
|
return -EINVAL;
|
|
if (value > 3600000)
|
|
return -EINVAL;
|
|
opts->read_timeout_ms = value;
|
|
break;
|
|
case Opt_readahead_pages:
|
|
if (match_int(&args[0], &value))
|
|
return -EINVAL;
|
|
opts->readahead_pages = value;
|
|
break;
|
|
case Opt_rlog_pages:
|
|
if (match_int(&args[0], &value))
|
|
return -EINVAL;
|
|
opts->read_log_pages = value;
|
|
break;
|
|
case Opt_rlog_wakeup_cnt:
|
|
if (match_int(&args[0], &value))
|
|
return -EINVAL;
|
|
opts->read_log_wakeup_count = value;
|
|
break;
|
|
case Opt_report_uid:
|
|
opts->report_uid = true;
|
|
break;
|
|
case Opt_sysfs_name:
|
|
opts->sysfs_name = match_strdup(&args[0]);
|
|
break;
|
|
default:
|
|
free_options(opts);
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Read file size from the attribute. Quicker than reading the header */
|
|
static u64 read_size_attr(struct dentry *backing_dentry)
|
|
{
|
|
__le64 attr_value;
|
|
ssize_t bytes_read;
|
|
|
|
bytes_read = vfs_getxattr(backing_dentry, INCFS_XATTR_SIZE_NAME,
|
|
(char *)&attr_value, sizeof(attr_value));
|
|
|
|
if (bytes_read != sizeof(attr_value))
|
|
return 0;
|
|
|
|
return le64_to_cpu(attr_value);
|
|
}
|
|
|
|
/* Read verity flag from the attribute. Quicker than reading the header */
|
|
static bool read_verity_attr(struct dentry *backing_dentry)
|
|
{
|
|
return vfs_getxattr(backing_dentry, INCFS_XATTR_VERITY_NAME, NULL, 0)
|
|
>= 0;
|
|
}
|
|
|
|
static int inode_test(struct inode *inode, void *opaque)
|
|
{
|
|
struct inode_search *search = opaque;
|
|
struct inode_info *node = get_incfs_node(inode);
|
|
struct inode *backing_inode = d_inode(search->backing_dentry);
|
|
|
|
if (!node)
|
|
return 0;
|
|
|
|
return node->n_backing_inode == backing_inode &&
|
|
inode->i_ino == search->ino;
|
|
}
|
|
|
|
static int inode_set(struct inode *inode, void *opaque)
|
|
{
|
|
struct inode_search *search = opaque;
|
|
struct inode_info *node = get_incfs_node(inode);
|
|
struct dentry *backing_dentry = search->backing_dentry;
|
|
struct inode *backing_inode = d_inode(backing_dentry);
|
|
|
|
fsstack_copy_attr_all(inode, backing_inode);
|
|
if (S_ISREG(inode->i_mode)) {
|
|
u64 size = search->size;
|
|
|
|
inode->i_size = size;
|
|
inode->i_blocks = get_blocks_count_for_size(size);
|
|
inode->i_mapping->a_ops = &incfs_address_space_ops;
|
|
inode->i_op = &incfs_file_inode_ops;
|
|
inode->i_fop = &incfs_file_ops;
|
|
inode->i_mode &= ~0222;
|
|
if (search->verity)
|
|
inode_set_flags(inode, S_VERITY, S_VERITY);
|
|
} else if (S_ISDIR(inode->i_mode)) {
|
|
inode->i_size = 0;
|
|
inode->i_blocks = 1;
|
|
inode->i_mapping->a_ops = &incfs_address_space_ops;
|
|
inode->i_op = &incfs_dir_inode_ops;
|
|
inode->i_fop = &incfs_dir_fops;
|
|
} else {
|
|
pr_warn_once("incfs: Unexpected inode type\n");
|
|
return -EBADF;
|
|
}
|
|
|
|
ihold(backing_inode);
|
|
node->n_backing_inode = backing_inode;
|
|
node->n_mount_info = get_mount_info(inode->i_sb);
|
|
inode->i_ctime = backing_inode->i_ctime;
|
|
inode->i_mtime = backing_inode->i_mtime;
|
|
inode->i_atime = backing_inode->i_atime;
|
|
inode->i_ino = backing_inode->i_ino;
|
|
if (backing_inode->i_ino < INCFS_START_INO_RANGE) {
|
|
pr_warn("incfs: ino conflict with backing FS %ld\n",
|
|
backing_inode->i_ino);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct inode *fetch_regular_inode(struct super_block *sb,
|
|
struct dentry *backing_dentry)
|
|
{
|
|
struct inode *backing_inode = d_inode(backing_dentry);
|
|
struct inode_search search = {
|
|
.ino = backing_inode->i_ino,
|
|
.backing_dentry = backing_dentry,
|
|
.size = read_size_attr(backing_dentry),
|
|
.verity = read_verity_attr(backing_dentry),
|
|
};
|
|
struct inode *inode = iget5_locked(sb, search.ino, inode_test,
|
|
inode_set, &search);
|
|
|
|
if (!inode)
|
|
return ERR_PTR(-ENOMEM);
|
|
|
|
if (inode->i_state & I_NEW)
|
|
unlock_new_inode(inode);
|
|
|
|
return inode;
|
|
}
|
|
|
|
static int iterate_incfs_dir(struct file *file, struct dir_context *ctx)
|
|
{
|
|
struct dir_file *dir = get_incfs_dir_file(file);
|
|
int error = 0;
|
|
struct mount_info *mi = get_mount_info(file_superblock(file));
|
|
bool root;
|
|
|
|
if (!dir) {
|
|
error = -EBADF;
|
|
goto out;
|
|
}
|
|
|
|
root = dir->backing_dir->f_inode
|
|
== d_inode(mi->mi_backing_dir_path.dentry);
|
|
|
|
if (root) {
|
|
error = emit_pseudo_files(ctx);
|
|
if (error)
|
|
goto out;
|
|
}
|
|
|
|
ctx->pos -= PSEUDO_FILE_COUNT;
|
|
error = iterate_dir(dir->backing_dir, ctx);
|
|
ctx->pos += PSEUDO_FILE_COUNT;
|
|
file->f_pos = dir->backing_dir->f_pos;
|
|
out:
|
|
if (error)
|
|
pr_warn("incfs: %s %s %d\n", __func__,
|
|
file->f_path.dentry->d_name.name, error);
|
|
return error;
|
|
}
|
|
|
|
static int incfs_init_dentry(struct dentry *dentry, struct path *path)
|
|
{
|
|
struct dentry_info *d_info = NULL;
|
|
|
|
if (!dentry || !path)
|
|
return -EFAULT;
|
|
|
|
d_info = kzalloc(sizeof(*d_info), GFP_NOFS);
|
|
if (!d_info)
|
|
return -ENOMEM;
|
|
|
|
d_info->backing_path = *path;
|
|
path_get(path);
|
|
|
|
dentry->d_fsdata = d_info;
|
|
return 0;
|
|
}
|
|
|
|
static struct dentry *open_or_create_special_dir(struct dentry *backing_dir,
|
|
const char *name,
|
|
bool *created)
|
|
{
|
|
struct dentry *index_dentry;
|
|
struct inode *backing_inode = d_inode(backing_dir);
|
|
int err = 0;
|
|
|
|
index_dentry = incfs_lookup_dentry(backing_dir, name);
|
|
if (!index_dentry) {
|
|
return ERR_PTR(-EINVAL);
|
|
} else if (IS_ERR(index_dentry)) {
|
|
return index_dentry;
|
|
} else if (d_really_is_positive(index_dentry)) {
|
|
/* Index already exists. */
|
|
*created = false;
|
|
return index_dentry;
|
|
}
|
|
|
|
/* Index needs to be created. */
|
|
inode_lock_nested(backing_inode, I_MUTEX_PARENT);
|
|
err = vfs_mkdir(backing_inode, index_dentry, 0777);
|
|
inode_unlock(backing_inode);
|
|
|
|
if (err) {
|
|
dput(index_dentry);
|
|
return ERR_PTR(err);
|
|
}
|
|
|
|
if (!d_really_is_positive(index_dentry) ||
|
|
unlikely(d_unhashed(index_dentry))) {
|
|
dput(index_dentry);
|
|
return ERR_PTR(-EINVAL);
|
|
}
|
|
|
|
*created = true;
|
|
return index_dentry;
|
|
}
|
|
|
|
static int read_single_page_timeouts(struct data_file *df, struct file *f,
|
|
int block_index, struct mem_range range,
|
|
struct mem_range tmp)
|
|
{
|
|
struct mount_info *mi = df->df_mount_info;
|
|
struct incfs_read_data_file_timeouts timeouts = {
|
|
.max_pending_time_us = U32_MAX,
|
|
};
|
|
int uid = current_uid().val;
|
|
int i;
|
|
|
|
spin_lock(&mi->mi_per_uid_read_timeouts_lock);
|
|
for (i = 0; i < mi->mi_per_uid_read_timeouts_size /
|
|
sizeof(*mi->mi_per_uid_read_timeouts); ++i) {
|
|
struct incfs_per_uid_read_timeouts *t =
|
|
&mi->mi_per_uid_read_timeouts[i];
|
|
|
|
if(t->uid == uid) {
|
|
timeouts.min_time_us = t->min_time_us;
|
|
timeouts.min_pending_time_us = t->min_pending_time_us;
|
|
timeouts.max_pending_time_us = t->max_pending_time_us;
|
|
break;
|
|
}
|
|
}
|
|
spin_unlock(&mi->mi_per_uid_read_timeouts_lock);
|
|
if (timeouts.max_pending_time_us == U32_MAX) {
|
|
u64 read_timeout_us = (u64)mi->mi_options.read_timeout_ms *
|
|
1000;
|
|
|
|
timeouts.max_pending_time_us = read_timeout_us <= U32_MAX ?
|
|
read_timeout_us : U32_MAX;
|
|
}
|
|
|
|
return incfs_read_data_file_block(range, f, block_index, tmp,
|
|
&timeouts);
|
|
}
|
|
|
|
static int read_single_page(struct file *f, struct page *page)
|
|
{
|
|
loff_t offset = 0;
|
|
loff_t size = 0;
|
|
ssize_t bytes_to_read = 0;
|
|
ssize_t read_result = 0;
|
|
struct data_file *df = get_incfs_data_file(f);
|
|
int result = 0;
|
|
void *page_start;
|
|
int block_index;
|
|
|
|
if (!df) {
|
|
SetPageError(page);
|
|
unlock_page(page);
|
|
return -EBADF;
|
|
}
|
|
|
|
page_start = kmap(page);
|
|
offset = page_offset(page);
|
|
block_index = (offset + df->df_mapped_offset) /
|
|
INCFS_DATA_FILE_BLOCK_SIZE;
|
|
size = df->df_size;
|
|
|
|
if (offset < size) {
|
|
struct mem_range tmp = {
|
|
.len = 2 * INCFS_DATA_FILE_BLOCK_SIZE
|
|
};
|
|
tmp.data = (u8 *)__get_free_pages(GFP_NOFS, get_order(tmp.len));
|
|
if (!tmp.data) {
|
|
read_result = -ENOMEM;
|
|
goto err;
|
|
}
|
|
bytes_to_read = min_t(loff_t, size - offset, PAGE_SIZE);
|
|
|
|
read_result = read_single_page_timeouts(df, f, block_index,
|
|
range(page_start, bytes_to_read), tmp);
|
|
|
|
free_pages((unsigned long)tmp.data, get_order(tmp.len));
|
|
} else {
|
|
bytes_to_read = 0;
|
|
read_result = 0;
|
|
}
|
|
|
|
err:
|
|
if (read_result < 0)
|
|
result = read_result;
|
|
else if (read_result < PAGE_SIZE)
|
|
zero_user(page, read_result, PAGE_SIZE - read_result);
|
|
|
|
if (result == 0)
|
|
SetPageUptodate(page);
|
|
else
|
|
SetPageError(page);
|
|
|
|
flush_dcache_page(page);
|
|
kunmap(page);
|
|
unlock_page(page);
|
|
return result;
|
|
}
|
|
|
|
int incfs_link(struct dentry *what, struct dentry *where)
|
|
{
|
|
struct dentry *parent_dentry = dget_parent(where);
|
|
struct inode *pinode = d_inode(parent_dentry);
|
|
int error = 0;
|
|
|
|
inode_lock_nested(pinode, I_MUTEX_PARENT);
|
|
error = vfs_link(what, pinode, where, NULL);
|
|
inode_unlock(pinode);
|
|
|
|
dput(parent_dentry);
|
|
return error;
|
|
}
|
|
|
|
int incfs_unlink(struct dentry *dentry)
|
|
{
|
|
struct dentry *parent_dentry = dget_parent(dentry);
|
|
struct inode *pinode = d_inode(parent_dentry);
|
|
int error = 0;
|
|
|
|
inode_lock_nested(pinode, I_MUTEX_PARENT);
|
|
error = vfs_unlink(pinode, dentry, NULL);
|
|
inode_unlock(pinode);
|
|
|
|
dput(parent_dentry);
|
|
return error;
|
|
}
|
|
|
|
static int incfs_rmdir(struct dentry *dentry)
|
|
{
|
|
struct dentry *parent_dentry = dget_parent(dentry);
|
|
struct inode *pinode = d_inode(parent_dentry);
|
|
int error = 0;
|
|
|
|
inode_lock_nested(pinode, I_MUTEX_PARENT);
|
|
error = vfs_rmdir(pinode, dentry);
|
|
inode_unlock(pinode);
|
|
|
|
dput(parent_dentry);
|
|
return error;
|
|
}
|
|
|
|
static void notify_unlink(struct dentry *dentry, const char *file_id_str,
|
|
const char *special_directory)
|
|
{
|
|
struct dentry *root = dentry;
|
|
struct dentry *file = NULL;
|
|
struct dentry *dir = NULL;
|
|
int error = 0;
|
|
bool take_lock = root->d_parent != root->d_parent->d_parent;
|
|
|
|
while (root != root->d_parent)
|
|
root = root->d_parent;
|
|
|
|
if (take_lock)
|
|
dir = incfs_lookup_dentry(root, special_directory);
|
|
else
|
|
dir = lookup_one_len(special_directory, root,
|
|
strlen(special_directory));
|
|
|
|
if (IS_ERR(dir)) {
|
|
error = PTR_ERR(dir);
|
|
goto out;
|
|
}
|
|
if (d_is_negative(dir)) {
|
|
error = -ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
file = incfs_lookup_dentry(dir, file_id_str);
|
|
if (IS_ERR(file)) {
|
|
error = PTR_ERR(file);
|
|
goto out;
|
|
}
|
|
if (d_is_negative(file)) {
|
|
error = -ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
fsnotify_unlink(d_inode(dir), file);
|
|
d_delete(file);
|
|
|
|
out:
|
|
if (error)
|
|
pr_warn("%s failed with error %d\n", __func__, error);
|
|
|
|
dput(dir);
|
|
dput(file);
|
|
}
|
|
|
|
static void maybe_delete_incomplete_file(struct file *f,
|
|
struct data_file *df)
|
|
{
|
|
struct backing_file_context *bfc;
|
|
struct mount_info *mi = df->df_mount_info;
|
|
char *file_id_str = NULL;
|
|
struct dentry *incomplete_file_dentry = NULL;
|
|
const struct cred *old_cred = override_creds(mi->mi_owner);
|
|
int error;
|
|
|
|
if (atomic_read(&df->df_data_blocks_written) < df->df_data_block_count)
|
|
goto out;
|
|
|
|
/* Truncate file to remove any preallocated space */
|
|
bfc = df->df_backing_file_context;
|
|
if (bfc) {
|
|
struct file *f = bfc->bc_file;
|
|
|
|
if (f) {
|
|
loff_t size = i_size_read(file_inode(f));
|
|
|
|
error = vfs_truncate(&f->f_path, size);
|
|
if (error)
|
|
/* No useful action on failure */
|
|
pr_warn("incfs: Failed to truncate complete file: %d\n",
|
|
error);
|
|
}
|
|
}
|
|
|
|
/* This is best effort - there is no useful action to take on failure */
|
|
file_id_str = file_id_to_str(df->df_id);
|
|
if (!file_id_str)
|
|
goto out;
|
|
|
|
incomplete_file_dentry = incfs_lookup_dentry(
|
|
df->df_mount_info->mi_incomplete_dir,
|
|
file_id_str);
|
|
if (!incomplete_file_dentry || IS_ERR(incomplete_file_dentry)) {
|
|
incomplete_file_dentry = NULL;
|
|
goto out;
|
|
}
|
|
|
|
if (!d_really_is_positive(incomplete_file_dentry))
|
|
goto out;
|
|
|
|
vfs_fsync(df->df_backing_file_context->bc_file, 0);
|
|
error = incfs_unlink(incomplete_file_dentry);
|
|
if (error) {
|
|
pr_warn("incfs: Deleting incomplete file failed: %d\n", error);
|
|
goto out;
|
|
}
|
|
|
|
notify_unlink(f->f_path.dentry, file_id_str, INCFS_INCOMPLETE_NAME);
|
|
|
|
out:
|
|
dput(incomplete_file_dentry);
|
|
kfree(file_id_str);
|
|
revert_creds(old_cred);
|
|
}
|
|
|
|
static long ioctl_fill_blocks(struct file *f, void __user *arg)
|
|
{
|
|
struct incfs_fill_blocks __user *usr_fill_blocks = arg;
|
|
struct incfs_fill_blocks fill_blocks;
|
|
struct incfs_fill_block __user *usr_fill_block_array;
|
|
struct data_file *df = get_incfs_data_file(f);
|
|
struct incfs_file_data *fd = f->private_data;
|
|
const ssize_t data_buf_size = 2 * INCFS_DATA_FILE_BLOCK_SIZE;
|
|
u8 *data_buf = NULL;
|
|
ssize_t error = 0;
|
|
int i = 0;
|
|
|
|
if (!df)
|
|
return -EBADF;
|
|
|
|
if (!fd || fd->fd_fill_permission != CAN_FILL)
|
|
return -EPERM;
|
|
|
|
if (copy_from_user(&fill_blocks, usr_fill_blocks, sizeof(fill_blocks)))
|
|
return -EFAULT;
|
|
|
|
usr_fill_block_array = u64_to_user_ptr(fill_blocks.fill_blocks);
|
|
data_buf = (u8 *)__get_free_pages(GFP_NOFS | __GFP_COMP,
|
|
get_order(data_buf_size));
|
|
if (!data_buf)
|
|
return -ENOMEM;
|
|
|
|
for (i = 0; i < fill_blocks.count; i++) {
|
|
struct incfs_fill_block fill_block = {};
|
|
|
|
if (copy_from_user(&fill_block, &usr_fill_block_array[i],
|
|
sizeof(fill_block)) > 0) {
|
|
error = -EFAULT;
|
|
break;
|
|
}
|
|
|
|
if (fill_block.data_len > data_buf_size) {
|
|
error = -E2BIG;
|
|
break;
|
|
}
|
|
|
|
if (copy_from_user(data_buf, u64_to_user_ptr(fill_block.data),
|
|
fill_block.data_len) > 0) {
|
|
error = -EFAULT;
|
|
break;
|
|
}
|
|
fill_block.data = 0; /* To make sure nobody uses it. */
|
|
if (fill_block.flags & INCFS_BLOCK_FLAGS_HASH) {
|
|
error = incfs_process_new_hash_block(df, &fill_block,
|
|
data_buf);
|
|
} else {
|
|
error = incfs_process_new_data_block(df, &fill_block,
|
|
data_buf);
|
|
}
|
|
if (error)
|
|
break;
|
|
}
|
|
|
|
if (data_buf)
|
|
free_pages((unsigned long)data_buf, get_order(data_buf_size));
|
|
|
|
maybe_delete_incomplete_file(f, df);
|
|
|
|
/*
|
|
* Only report the error if no records were processed, otherwise
|
|
* just return how many were processed successfully.
|
|
*/
|
|
if (i == 0)
|
|
return error;
|
|
|
|
return i;
|
|
}
|
|
|
|
static long ioctl_read_file_signature(struct file *f, void __user *arg)
|
|
{
|
|
struct incfs_get_file_sig_args __user *args_usr_ptr = arg;
|
|
struct incfs_get_file_sig_args args = {};
|
|
u8 *sig_buffer = NULL;
|
|
size_t sig_buf_size = 0;
|
|
int error = 0;
|
|
int read_result = 0;
|
|
struct data_file *df = get_incfs_data_file(f);
|
|
|
|
if (!df)
|
|
return -EINVAL;
|
|
|
|
if (copy_from_user(&args, args_usr_ptr, sizeof(args)) > 0)
|
|
return -EINVAL;
|
|
|
|
sig_buf_size = args.file_signature_buf_size;
|
|
if (sig_buf_size > INCFS_MAX_SIGNATURE_SIZE)
|
|
return -E2BIG;
|
|
|
|
sig_buffer = kzalloc(sig_buf_size, GFP_NOFS | __GFP_COMP);
|
|
if (!sig_buffer)
|
|
return -ENOMEM;
|
|
|
|
read_result = incfs_read_file_signature(df,
|
|
range(sig_buffer, sig_buf_size));
|
|
|
|
if (read_result < 0) {
|
|
error = read_result;
|
|
goto out;
|
|
}
|
|
|
|
if (copy_to_user(u64_to_user_ptr(args.file_signature), sig_buffer,
|
|
read_result)) {
|
|
error = -EFAULT;
|
|
goto out;
|
|
}
|
|
|
|
args.file_signature_len_out = read_result;
|
|
if (copy_to_user(args_usr_ptr, &args, sizeof(args)))
|
|
error = -EFAULT;
|
|
|
|
out:
|
|
kfree(sig_buffer);
|
|
|
|
return error;
|
|
}
|
|
|
|
static long ioctl_get_filled_blocks(struct file *f, void __user *arg)
|
|
{
|
|
struct incfs_get_filled_blocks_args __user *args_usr_ptr = arg;
|
|
struct incfs_get_filled_blocks_args args = {};
|
|
struct data_file *df = get_incfs_data_file(f);
|
|
struct incfs_file_data *fd = f->private_data;
|
|
int error;
|
|
|
|
if (!df || !fd)
|
|
return -EINVAL;
|
|
|
|
if (fd->fd_fill_permission != CAN_FILL)
|
|
return -EPERM;
|
|
|
|
if (copy_from_user(&args, args_usr_ptr, sizeof(args)) > 0)
|
|
return -EINVAL;
|
|
|
|
error = incfs_get_filled_blocks(df, fd, &args);
|
|
|
|
if (copy_to_user(args_usr_ptr, &args, sizeof(args)))
|
|
return -EFAULT;
|
|
|
|
return error;
|
|
}
|
|
|
|
static long ioctl_get_block_count(struct file *f, void __user *arg)
|
|
{
|
|
struct incfs_get_block_count_args __user *args_usr_ptr = arg;
|
|
struct incfs_get_block_count_args args = {};
|
|
struct data_file *df = get_incfs_data_file(f);
|
|
|
|
if (!df)
|
|
return -EINVAL;
|
|
|
|
args.total_data_blocks_out = df->df_data_block_count;
|
|
args.filled_data_blocks_out = atomic_read(&df->df_data_blocks_written);
|
|
args.total_hash_blocks_out = df->df_total_block_count -
|
|
df->df_data_block_count;
|
|
args.filled_hash_blocks_out = atomic_read(&df->df_hash_blocks_written);
|
|
|
|
if (copy_to_user(args_usr_ptr, &args, sizeof(args)))
|
|
return -EFAULT;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int incfs_ioctl_get_flags(struct file *f, void __user *arg)
|
|
{
|
|
u32 flags = IS_VERITY(file_inode(f)) ? FS_VERITY_FL : 0;
|
|
|
|
return put_user(flags, (int __user *) arg);
|
|
}
|
|
|
|
static long dispatch_ioctl(struct file *f, unsigned int req, unsigned long arg)
|
|
{
|
|
switch (req) {
|
|
case INCFS_IOC_FILL_BLOCKS:
|
|
return ioctl_fill_blocks(f, (void __user *)arg);
|
|
case INCFS_IOC_READ_FILE_SIGNATURE:
|
|
return ioctl_read_file_signature(f, (void __user *)arg);
|
|
case INCFS_IOC_GET_FILLED_BLOCKS:
|
|
return ioctl_get_filled_blocks(f, (void __user *)arg);
|
|
case INCFS_IOC_GET_BLOCK_COUNT:
|
|
return ioctl_get_block_count(f, (void __user *)arg);
|
|
case FS_IOC_ENABLE_VERITY:
|
|
return incfs_ioctl_enable_verity(f, (const void __user *)arg);
|
|
case FS_IOC_GETFLAGS:
|
|
return incfs_ioctl_get_flags(f, (void __user *) arg);
|
|
case FS_IOC_MEASURE_VERITY:
|
|
return incfs_ioctl_measure_verity(f, (void __user *)arg);
|
|
case FS_IOC_READ_VERITY_METADATA:
|
|
return incfs_ioctl_read_verity_metadata(f, (void __user *)arg);
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
static long incfs_compat_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
switch (cmd) {
|
|
case FS_IOC32_GETFLAGS:
|
|
cmd = FS_IOC_GETFLAGS;
|
|
break;
|
|
case INCFS_IOC_FILL_BLOCKS:
|
|
case INCFS_IOC_READ_FILE_SIGNATURE:
|
|
case INCFS_IOC_GET_FILLED_BLOCKS:
|
|
case INCFS_IOC_GET_BLOCK_COUNT:
|
|
case FS_IOC_ENABLE_VERITY:
|
|
case FS_IOC_MEASURE_VERITY:
|
|
case FS_IOC_READ_VERITY_METADATA:
|
|
break;
|
|
default:
|
|
return -ENOIOCTLCMD;
|
|
}
|
|
return dispatch_ioctl(file, cmd, (unsigned long) compat_ptr(arg));
|
|
}
|
|
#endif
|
|
|
|
static struct dentry *dir_lookup(struct inode *dir_inode, struct dentry *dentry,
|
|
unsigned int flags)
|
|
{
|
|
struct mount_info *mi = get_mount_info(dir_inode->i_sb);
|
|
struct dentry *dir_dentry = NULL;
|
|
struct dentry *backing_dentry = NULL;
|
|
struct path dir_backing_path = {};
|
|
struct inode_info *dir_info = get_incfs_node(dir_inode);
|
|
int err = 0;
|
|
|
|
if (!mi || !dir_info || !dir_info->n_backing_inode)
|
|
return ERR_PTR(-EBADF);
|
|
|
|
if (d_inode(mi->mi_backing_dir_path.dentry) ==
|
|
dir_info->n_backing_inode) {
|
|
/* We do lookup in the FS root. Show pseudo files. */
|
|
err = dir_lookup_pseudo_files(dir_inode->i_sb, dentry);
|
|
if (err != -ENOENT)
|
|
goto out;
|
|
err = 0;
|
|
}
|
|
|
|
dir_dentry = dget_parent(dentry);
|
|
get_incfs_backing_path(dir_dentry, &dir_backing_path);
|
|
backing_dentry = incfs_lookup_dentry(dir_backing_path.dentry,
|
|
dentry->d_name.name);
|
|
|
|
if (!backing_dentry || IS_ERR(backing_dentry)) {
|
|
err = IS_ERR(backing_dentry)
|
|
? PTR_ERR(backing_dentry)
|
|
: -EFAULT;
|
|
backing_dentry = NULL;
|
|
goto out;
|
|
} else {
|
|
struct inode *inode = NULL;
|
|
struct path backing_path = {
|
|
.mnt = dir_backing_path.mnt,
|
|
.dentry = backing_dentry
|
|
};
|
|
|
|
err = incfs_init_dentry(dentry, &backing_path);
|
|
if (err)
|
|
goto out;
|
|
|
|
if (!d_really_is_positive(backing_dentry)) {
|
|
/*
|
|
* No such entry found in the backing dir.
|
|
* Create a negative entry.
|
|
*/
|
|
d_add(dentry, NULL);
|
|
err = 0;
|
|
goto out;
|
|
}
|
|
|
|
if (d_inode(backing_dentry)->i_sb !=
|
|
dir_info->n_backing_inode->i_sb) {
|
|
/*
|
|
* Somehow after the path lookup we ended up in a
|
|
* different fs mount. If we keep going it's going
|
|
* to end badly.
|
|
*/
|
|
err = -EXDEV;
|
|
goto out;
|
|
}
|
|
|
|
inode = fetch_regular_inode(dir_inode->i_sb, backing_dentry);
|
|
if (IS_ERR(inode)) {
|
|
err = PTR_ERR(inode);
|
|
goto out;
|
|
}
|
|
|
|
d_add(dentry, inode);
|
|
}
|
|
|
|
out:
|
|
dput(dir_dentry);
|
|
dput(backing_dentry);
|
|
path_put(&dir_backing_path);
|
|
if (err)
|
|
pr_debug("incfs: %s %s %d\n", __func__,
|
|
dentry->d_name.name, err);
|
|
return ERR_PTR(err);
|
|
}
|
|
|
|
static int dir_mkdir(struct inode *dir, struct dentry *dentry, umode_t mode)
|
|
{
|
|
struct mount_info *mi = get_mount_info(dir->i_sb);
|
|
struct inode_info *dir_node = get_incfs_node(dir);
|
|
struct dentry *backing_dentry = NULL;
|
|
struct path backing_path = {};
|
|
int err = 0;
|
|
|
|
|
|
if (!mi || !dir_node || !dir_node->n_backing_inode)
|
|
return -EBADF;
|
|
|
|
err = mutex_lock_interruptible(&mi->mi_dir_struct_mutex);
|
|
if (err)
|
|
return err;
|
|
|
|
get_incfs_backing_path(dentry, &backing_path);
|
|
backing_dentry = backing_path.dentry;
|
|
|
|
if (!backing_dentry) {
|
|
err = -EBADF;
|
|
goto path_err;
|
|
}
|
|
|
|
if (backing_dentry->d_parent == mi->mi_index_dir) {
|
|
/* Can't create a subdir inside .index */
|
|
err = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
if (backing_dentry->d_parent == mi->mi_incomplete_dir) {
|
|
/* Can't create a subdir inside .incomplete */
|
|
err = -EBUSY;
|
|
goto out;
|
|
}
|
|
inode_lock_nested(dir_node->n_backing_inode, I_MUTEX_PARENT);
|
|
err = vfs_mkdir(dir_node->n_backing_inode, backing_dentry, mode | 0222);
|
|
inode_unlock(dir_node->n_backing_inode);
|
|
if (!err) {
|
|
struct inode *inode = NULL;
|
|
|
|
if (d_really_is_negative(backing_dentry) ||
|
|
unlikely(d_unhashed(backing_dentry))) {
|
|
err = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
inode = fetch_regular_inode(dir->i_sb, backing_dentry);
|
|
if (IS_ERR(inode)) {
|
|
err = PTR_ERR(inode);
|
|
goto out;
|
|
}
|
|
d_instantiate(dentry, inode);
|
|
}
|
|
|
|
out:
|
|
if (d_really_is_negative(dentry))
|
|
d_drop(dentry);
|
|
path_put(&backing_path);
|
|
|
|
path_err:
|
|
mutex_unlock(&mi->mi_dir_struct_mutex);
|
|
if (err)
|
|
pr_debug("incfs: %s err:%d\n", __func__, err);
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Delete file referenced by backing_dentry and if appropriate its hardlink
|
|
* from .index and .incomplete
|
|
*/
|
|
static int file_delete(struct mount_info *mi, struct dentry *dentry,
|
|
struct dentry *backing_dentry, int nlink)
|
|
{
|
|
struct dentry *index_file_dentry = NULL;
|
|
struct dentry *incomplete_file_dentry = NULL;
|
|
/* 2 chars per byte of file ID + 1 char for \0 */
|
|
char file_id_str[2 * sizeof(incfs_uuid_t) + 1] = {0};
|
|
ssize_t uuid_size = 0;
|
|
int error = 0;
|
|
|
|
WARN_ON(!mutex_is_locked(&mi->mi_dir_struct_mutex));
|
|
|
|
if (nlink > 3)
|
|
goto just_unlink;
|
|
|
|
uuid_size = vfs_getxattr(backing_dentry, INCFS_XATTR_ID_NAME,
|
|
file_id_str, 2 * sizeof(incfs_uuid_t));
|
|
if (uuid_size < 0) {
|
|
error = uuid_size;
|
|
goto out;
|
|
}
|
|
|
|
if (uuid_size != 2 * sizeof(incfs_uuid_t)) {
|
|
error = -EBADMSG;
|
|
goto out;
|
|
}
|
|
|
|
index_file_dentry = incfs_lookup_dentry(mi->mi_index_dir, file_id_str);
|
|
if (IS_ERR(index_file_dentry)) {
|
|
error = PTR_ERR(index_file_dentry);
|
|
index_file_dentry = NULL;
|
|
goto out;
|
|
}
|
|
|
|
if (d_really_is_positive(index_file_dentry) && nlink > 0)
|
|
nlink--;
|
|
|
|
if (nlink > 2)
|
|
goto just_unlink;
|
|
|
|
incomplete_file_dentry = incfs_lookup_dentry(mi->mi_incomplete_dir,
|
|
file_id_str);
|
|
if (IS_ERR(incomplete_file_dentry)) {
|
|
error = PTR_ERR(incomplete_file_dentry);
|
|
incomplete_file_dentry = NULL;
|
|
goto out;
|
|
}
|
|
|
|
if (d_really_is_positive(incomplete_file_dentry) && nlink > 0)
|
|
nlink--;
|
|
|
|
if (nlink > 1)
|
|
goto just_unlink;
|
|
|
|
if (d_really_is_positive(index_file_dentry)) {
|
|
error = incfs_unlink(index_file_dentry);
|
|
if (error)
|
|
goto out;
|
|
notify_unlink(dentry, file_id_str, INCFS_INDEX_NAME);
|
|
}
|
|
|
|
if (d_really_is_positive(incomplete_file_dentry)) {
|
|
error = incfs_unlink(incomplete_file_dentry);
|
|
if (error)
|
|
goto out;
|
|
notify_unlink(dentry, file_id_str, INCFS_INCOMPLETE_NAME);
|
|
}
|
|
|
|
just_unlink:
|
|
error = incfs_unlink(backing_dentry);
|
|
|
|
out:
|
|
dput(index_file_dentry);
|
|
dput(incomplete_file_dentry);
|
|
if (error)
|
|
pr_debug("incfs: delete_file_from_index err:%d\n", error);
|
|
return error;
|
|
}
|
|
|
|
static int dir_unlink(struct inode *dir, struct dentry *dentry)
|
|
{
|
|
struct mount_info *mi = get_mount_info(dir->i_sb);
|
|
struct path backing_path = {};
|
|
struct kstat stat;
|
|
int err = 0;
|
|
|
|
if (!mi)
|
|
return -EBADF;
|
|
|
|
err = mutex_lock_interruptible(&mi->mi_dir_struct_mutex);
|
|
if (err)
|
|
return err;
|
|
|
|
get_incfs_backing_path(dentry, &backing_path);
|
|
if (!backing_path.dentry) {
|
|
err = -EBADF;
|
|
goto path_err;
|
|
}
|
|
|
|
if (backing_path.dentry->d_parent == mi->mi_index_dir) {
|
|
/* Direct unlink from .index are not allowed. */
|
|
err = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
if (backing_path.dentry->d_parent == mi->mi_incomplete_dir) {
|
|
/* Direct unlink from .incomplete are not allowed. */
|
|
err = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
err = vfs_getattr(&backing_path, &stat, STATX_NLINK,
|
|
AT_STATX_SYNC_AS_STAT);
|
|
if (err)
|
|
goto out;
|
|
|
|
err = file_delete(mi, dentry, backing_path.dentry, stat.nlink);
|
|
|
|
d_drop(dentry);
|
|
out:
|
|
path_put(&backing_path);
|
|
path_err:
|
|
if (err)
|
|
pr_debug("incfs: %s err:%d\n", __func__, err);
|
|
mutex_unlock(&mi->mi_dir_struct_mutex);
|
|
return err;
|
|
}
|
|
|
|
static int dir_link(struct dentry *old_dentry, struct inode *dir,
|
|
struct dentry *new_dentry)
|
|
{
|
|
struct mount_info *mi = get_mount_info(dir->i_sb);
|
|
struct path backing_old_path = {};
|
|
struct path backing_new_path = {};
|
|
int error = 0;
|
|
|
|
if (!mi)
|
|
return -EBADF;
|
|
|
|
error = mutex_lock_interruptible(&mi->mi_dir_struct_mutex);
|
|
if (error)
|
|
return error;
|
|
|
|
get_incfs_backing_path(old_dentry, &backing_old_path);
|
|
get_incfs_backing_path(new_dentry, &backing_new_path);
|
|
|
|
if (backing_new_path.dentry->d_parent == mi->mi_index_dir) {
|
|
/* Can't link to .index */
|
|
error = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
if (backing_new_path.dentry->d_parent == mi->mi_incomplete_dir) {
|
|
/* Can't link to .incomplete */
|
|
error = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
error = incfs_link(backing_old_path.dentry, backing_new_path.dentry);
|
|
if (!error) {
|
|
struct inode *inode = NULL;
|
|
struct dentry *bdentry = backing_new_path.dentry;
|
|
|
|
if (d_really_is_negative(bdentry)) {
|
|
error = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
inode = fetch_regular_inode(dir->i_sb, bdentry);
|
|
if (IS_ERR(inode)) {
|
|
error = PTR_ERR(inode);
|
|
goto out;
|
|
}
|
|
d_instantiate(new_dentry, inode);
|
|
}
|
|
|
|
out:
|
|
path_put(&backing_old_path);
|
|
path_put(&backing_new_path);
|
|
if (error)
|
|
pr_debug("incfs: %s err:%d\n", __func__, error);
|
|
mutex_unlock(&mi->mi_dir_struct_mutex);
|
|
return error;
|
|
}
|
|
|
|
static int dir_rmdir(struct inode *dir, struct dentry *dentry)
|
|
{
|
|
struct mount_info *mi = get_mount_info(dir->i_sb);
|
|
struct path backing_path = {};
|
|
int err = 0;
|
|
|
|
if (!mi)
|
|
return -EBADF;
|
|
|
|
err = mutex_lock_interruptible(&mi->mi_dir_struct_mutex);
|
|
if (err)
|
|
return err;
|
|
|
|
get_incfs_backing_path(dentry, &backing_path);
|
|
if (!backing_path.dentry) {
|
|
err = -EBADF;
|
|
goto path_err;
|
|
}
|
|
|
|
if (backing_path.dentry == mi->mi_index_dir) {
|
|
/* Can't delete .index */
|
|
err = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
if (backing_path.dentry == mi->mi_incomplete_dir) {
|
|
/* Can't delete .incomplete */
|
|
err = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
err = incfs_rmdir(backing_path.dentry);
|
|
if (!err)
|
|
d_drop(dentry);
|
|
out:
|
|
path_put(&backing_path);
|
|
|
|
path_err:
|
|
if (err)
|
|
pr_debug("incfs: %s err:%d\n", __func__, err);
|
|
mutex_unlock(&mi->mi_dir_struct_mutex);
|
|
return err;
|
|
}
|
|
|
|
static int dir_rename(struct inode *old_dir, struct dentry *old_dentry,
|
|
struct inode *new_dir, struct dentry *new_dentry)
|
|
{
|
|
struct mount_info *mi = get_mount_info(old_dir->i_sb);
|
|
struct dentry *backing_old_dentry;
|
|
struct dentry *backing_new_dentry;
|
|
struct dentry *backing_old_dir_dentry;
|
|
struct dentry *backing_new_dir_dentry;
|
|
struct inode *target_inode;
|
|
struct dentry *trap;
|
|
int error = 0;
|
|
|
|
error = mutex_lock_interruptible(&mi->mi_dir_struct_mutex);
|
|
if (error)
|
|
return error;
|
|
|
|
backing_old_dentry = get_incfs_dentry(old_dentry)->backing_path.dentry;
|
|
|
|
if (!backing_old_dentry || backing_old_dentry == mi->mi_index_dir ||
|
|
backing_old_dentry == mi->mi_incomplete_dir) {
|
|
/* Renaming .index or .incomplete not allowed */
|
|
error = -EBUSY;
|
|
goto exit;
|
|
}
|
|
|
|
backing_new_dentry = get_incfs_dentry(new_dentry)->backing_path.dentry;
|
|
dget(backing_old_dentry);
|
|
dget(backing_new_dentry);
|
|
|
|
backing_old_dir_dentry = dget_parent(backing_old_dentry);
|
|
backing_new_dir_dentry = dget_parent(backing_new_dentry);
|
|
target_inode = d_inode(new_dentry);
|
|
|
|
if (backing_old_dir_dentry == mi->mi_index_dir ||
|
|
backing_old_dir_dentry == mi->mi_incomplete_dir) {
|
|
/* Direct moves from .index or .incomplete are not allowed. */
|
|
error = -EBUSY;
|
|
goto out;
|
|
}
|
|
|
|
trap = lock_rename(backing_old_dir_dentry, backing_new_dir_dentry);
|
|
|
|
if (trap == backing_old_dentry) {
|
|
error = -EINVAL;
|
|
goto unlock_out;
|
|
}
|
|
if (trap == backing_new_dentry) {
|
|
error = -ENOTEMPTY;
|
|
goto unlock_out;
|
|
}
|
|
|
|
error = vfs_rename(d_inode(backing_old_dir_dentry), backing_old_dentry,
|
|
d_inode(backing_new_dir_dentry), backing_new_dentry,
|
|
NULL, 0);
|
|
if (error)
|
|
goto unlock_out;
|
|
if (target_inode)
|
|
fsstack_copy_attr_all(target_inode,
|
|
get_incfs_node(target_inode)->n_backing_inode);
|
|
fsstack_copy_attr_all(new_dir, d_inode(backing_new_dir_dentry));
|
|
if (new_dir != old_dir)
|
|
fsstack_copy_attr_all(old_dir, d_inode(backing_old_dir_dentry));
|
|
|
|
unlock_out:
|
|
unlock_rename(backing_old_dir_dentry, backing_new_dir_dentry);
|
|
|
|
out:
|
|
dput(backing_new_dir_dentry);
|
|
dput(backing_old_dir_dentry);
|
|
dput(backing_new_dentry);
|
|
dput(backing_old_dentry);
|
|
|
|
exit:
|
|
mutex_unlock(&mi->mi_dir_struct_mutex);
|
|
if (error)
|
|
pr_debug("incfs: %s err:%d\n", __func__, error);
|
|
return error;
|
|
}
|
|
|
|
|
|
static int file_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct mount_info *mi = get_mount_info(inode->i_sb);
|
|
struct file *backing_file = NULL;
|
|
struct path backing_path = {};
|
|
int err = 0;
|
|
int flags = O_NOATIME | O_LARGEFILE |
|
|
(S_ISDIR(inode->i_mode) ? O_RDONLY : O_RDWR);
|
|
const struct cred *old_cred;
|
|
|
|
WARN_ON(file->private_data);
|
|
|
|
if (!mi)
|
|
return -EBADF;
|
|
|
|
get_incfs_backing_path(file->f_path.dentry, &backing_path);
|
|
if (!backing_path.dentry)
|
|
return -EBADF;
|
|
|
|
old_cred = override_creds(mi->mi_owner);
|
|
backing_file = dentry_open(&backing_path, flags, current_cred());
|
|
revert_creds(old_cred);
|
|
path_put(&backing_path);
|
|
|
|
if (IS_ERR(backing_file)) {
|
|
err = PTR_ERR(backing_file);
|
|
backing_file = NULL;
|
|
goto out;
|
|
}
|
|
|
|
if (S_ISREG(inode->i_mode)) {
|
|
struct incfs_file_data *fd = kzalloc(sizeof(*fd), GFP_NOFS);
|
|
|
|
if (!fd) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
*fd = (struct incfs_file_data) {
|
|
.fd_fill_permission = CANT_FILL,
|
|
};
|
|
file->private_data = fd;
|
|
|
|
err = make_inode_ready_for_data_ops(mi, inode, backing_file);
|
|
if (err)
|
|
goto out;
|
|
|
|
err = incfs_fsverity_file_open(inode, file);
|
|
if (err)
|
|
goto out;
|
|
} else if (S_ISDIR(inode->i_mode)) {
|
|
struct dir_file *dir = NULL;
|
|
|
|
dir = incfs_open_dir_file(mi, backing_file);
|
|
if (IS_ERR(dir))
|
|
err = PTR_ERR(dir);
|
|
else
|
|
file->private_data = dir;
|
|
} else
|
|
err = -EBADF;
|
|
|
|
out:
|
|
if (err) {
|
|
pr_debug("name:%s err: %d\n",
|
|
file->f_path.dentry->d_name.name, err);
|
|
if (S_ISREG(inode->i_mode))
|
|
kfree(file->private_data);
|
|
else if (S_ISDIR(inode->i_mode))
|
|
incfs_free_dir_file(file->private_data);
|
|
|
|
file->private_data = NULL;
|
|
}
|
|
|
|
if (backing_file)
|
|
fput(backing_file);
|
|
return err;
|
|
}
|
|
|
|
static int file_release(struct inode *inode, struct file *file)
|
|
{
|
|
if (S_ISREG(inode->i_mode)) {
|
|
kfree(file->private_data);
|
|
file->private_data = NULL;
|
|
} else if (S_ISDIR(inode->i_mode)) {
|
|
struct dir_file *dir = get_incfs_dir_file(file);
|
|
|
|
incfs_free_dir_file(dir);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dentry_revalidate(struct dentry *d, unsigned int flags)
|
|
{
|
|
struct path backing_path = {};
|
|
struct inode_info *info = get_incfs_node(d_inode(d));
|
|
struct inode *binode = (info == NULL) ? NULL : info->n_backing_inode;
|
|
struct dentry *backing_dentry = NULL;
|
|
int result = 0;
|
|
|
|
if (flags & LOOKUP_RCU)
|
|
return -ECHILD;
|
|
|
|
get_incfs_backing_path(d, &backing_path);
|
|
backing_dentry = backing_path.dentry;
|
|
if (!backing_dentry)
|
|
goto out;
|
|
|
|
if (d_inode(backing_dentry) != binode) {
|
|
/*
|
|
* Backing inodes obtained via dentry and inode don't match.
|
|
* It indicates that most likely backing dir has changed
|
|
* directly bypassing Incremental FS interface.
|
|
*/
|
|
goto out;
|
|
}
|
|
|
|
if (backing_dentry->d_flags & DCACHE_OP_REVALIDATE) {
|
|
result = backing_dentry->d_op->d_revalidate(backing_dentry,
|
|
flags);
|
|
} else
|
|
result = 1;
|
|
|
|
out:
|
|
path_put(&backing_path);
|
|
return result;
|
|
}
|
|
|
|
static void dentry_release(struct dentry *d)
|
|
{
|
|
struct dentry_info *di = get_incfs_dentry(d);
|
|
|
|
if (di)
|
|
path_put(&di->backing_path);
|
|
kfree(d->d_fsdata);
|
|
d->d_fsdata = NULL;
|
|
}
|
|
|
|
static struct inode *alloc_inode(struct super_block *sb)
|
|
{
|
|
struct inode_info *node = kzalloc(sizeof(*node), GFP_NOFS);
|
|
|
|
/* TODO: add a slab-based cache here. */
|
|
if (!node)
|
|
return NULL;
|
|
inode_init_once(&node->n_vfs_inode);
|
|
return &node->n_vfs_inode;
|
|
}
|
|
|
|
static void free_inode(struct inode *inode)
|
|
{
|
|
struct inode_info *node = get_incfs_node(inode);
|
|
|
|
kfree(node);
|
|
}
|
|
|
|
static void evict_inode(struct inode *inode)
|
|
{
|
|
struct inode_info *node = get_incfs_node(inode);
|
|
|
|
if (node) {
|
|
if (node->n_backing_inode) {
|
|
iput(node->n_backing_inode);
|
|
node->n_backing_inode = NULL;
|
|
}
|
|
if (node->n_file) {
|
|
incfs_free_data_file(node->n_file);
|
|
node->n_file = NULL;
|
|
}
|
|
}
|
|
|
|
truncate_inode_pages(&inode->i_data, 0);
|
|
clear_inode(inode);
|
|
}
|
|
|
|
static int incfs_setattr(struct dentry *dentry, struct iattr *ia)
|
|
{
|
|
struct dentry_info *di = get_incfs_dentry(dentry);
|
|
struct dentry *backing_dentry;
|
|
struct inode *backing_inode;
|
|
int error;
|
|
|
|
if (ia->ia_valid & ATTR_SIZE)
|
|
return -EINVAL;
|
|
|
|
if (!di)
|
|
return -EINVAL;
|
|
backing_dentry = di->backing_path.dentry;
|
|
if (!backing_dentry)
|
|
return -EINVAL;
|
|
|
|
backing_inode = d_inode(backing_dentry);
|
|
|
|
/* incfs files are readonly, but the backing files must be writeable */
|
|
if (S_ISREG(backing_inode->i_mode)) {
|
|
if ((ia->ia_valid & ATTR_MODE) && (ia->ia_mode & 0222))
|
|
return -EINVAL;
|
|
|
|
ia->ia_mode |= 0222;
|
|
}
|
|
|
|
inode_lock(d_inode(backing_dentry));
|
|
error = notify_change(backing_dentry, ia, NULL);
|
|
inode_unlock(d_inode(backing_dentry));
|
|
|
|
if (error)
|
|
return error;
|
|
|
|
if (S_ISREG(backing_inode->i_mode))
|
|
ia->ia_mode &= ~0222;
|
|
|
|
return simple_setattr(dentry, ia);
|
|
}
|
|
|
|
|
|
static int incfs_getattr(const struct path *path,
|
|
struct kstat *stat, u32 request_mask,
|
|
unsigned int query_flags)
|
|
{
|
|
struct inode *inode = d_inode(path->dentry);
|
|
|
|
generic_fillattr(inode, stat);
|
|
|
|
if (inode->i_ino < INCFS_START_INO_RANGE)
|
|
return 0;
|
|
|
|
stat->attributes &= ~STATX_ATTR_VERITY;
|
|
if (IS_VERITY(inode))
|
|
stat->attributes |= STATX_ATTR_VERITY;
|
|
stat->attributes_mask |= STATX_ATTR_VERITY;
|
|
|
|
if (request_mask & STATX_BLOCKS) {
|
|
struct kstat backing_kstat;
|
|
struct dentry_info *di = get_incfs_dentry(path->dentry);
|
|
int error = 0;
|
|
struct path *backing_path;
|
|
|
|
if (!di)
|
|
return -EFSCORRUPTED;
|
|
backing_path = &di->backing_path;
|
|
error = vfs_getattr(backing_path, &backing_kstat, STATX_BLOCKS,
|
|
AT_STATX_SYNC_AS_STAT);
|
|
if (error)
|
|
return error;
|
|
|
|
stat->blocks = backing_kstat.blocks;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t incfs_getxattr(struct dentry *d, const char *name,
|
|
void *value, size_t size)
|
|
{
|
|
struct dentry_info *di = get_incfs_dentry(d);
|
|
struct mount_info *mi = get_mount_info(d->d_sb);
|
|
char *stored_value;
|
|
size_t stored_size;
|
|
int i;
|
|
|
|
if (di && di->backing_path.dentry)
|
|
return vfs_getxattr(di->backing_path.dentry, name, value, size);
|
|
|
|
if (strcmp(name, "security.selinux"))
|
|
return -ENODATA;
|
|
|
|
for (i = 0; i < PSEUDO_FILE_COUNT; ++i)
|
|
if (!strcmp(d->d_iname, incfs_pseudo_file_names[i].data))
|
|
break;
|
|
if (i == PSEUDO_FILE_COUNT)
|
|
return -ENODATA;
|
|
|
|
stored_value = mi->pseudo_file_xattr[i].data;
|
|
stored_size = mi->pseudo_file_xattr[i].len;
|
|
if (!stored_value)
|
|
return -ENODATA;
|
|
|
|
if (stored_size > size)
|
|
return -E2BIG;
|
|
|
|
memcpy(value, stored_value, stored_size);
|
|
return stored_size;
|
|
}
|
|
|
|
|
|
static ssize_t incfs_setxattr(struct dentry *d, const char *name,
|
|
const void *value, size_t size, int flags)
|
|
{
|
|
struct dentry_info *di = get_incfs_dentry(d);
|
|
struct mount_info *mi = get_mount_info(d->d_sb);
|
|
u8 **stored_value;
|
|
size_t *stored_size;
|
|
int i;
|
|
|
|
if (di && di->backing_path.dentry)
|
|
return vfs_setxattr(di->backing_path.dentry, name, value, size,
|
|
flags);
|
|
|
|
if (strcmp(name, "security.selinux"))
|
|
return -ENODATA;
|
|
|
|
if (size > INCFS_MAX_FILE_ATTR_SIZE)
|
|
return -E2BIG;
|
|
|
|
for (i = 0; i < PSEUDO_FILE_COUNT; ++i)
|
|
if (!strcmp(d->d_iname, incfs_pseudo_file_names[i].data))
|
|
break;
|
|
if (i == PSEUDO_FILE_COUNT)
|
|
return -ENODATA;
|
|
|
|
stored_value = &mi->pseudo_file_xattr[i].data;
|
|
stored_size = &mi->pseudo_file_xattr[i].len;
|
|
kfree (*stored_value);
|
|
*stored_value = kzalloc(size, GFP_NOFS);
|
|
if (!*stored_value)
|
|
return -ENOMEM;
|
|
|
|
memcpy(*stored_value, value, size);
|
|
*stored_size = size;
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t incfs_listxattr(struct dentry *d, char *list, size_t size)
|
|
{
|
|
struct dentry_info *di = get_incfs_dentry(d);
|
|
|
|
if (!di || !di->backing_path.dentry)
|
|
return -ENODATA;
|
|
|
|
return vfs_listxattr(di->backing_path.dentry, list, size);
|
|
}
|
|
|
|
struct dentry *incfs_mount_fs(struct file_system_type *type, int flags,
|
|
const char *dev_name, void *data)
|
|
{
|
|
struct mount_options options = {};
|
|
struct mount_info *mi = NULL;
|
|
struct path backing_dir_path = {};
|
|
struct dentry *index_dir = NULL;
|
|
struct dentry *incomplete_dir = NULL;
|
|
struct super_block *src_fs_sb = NULL;
|
|
struct inode *root_inode = NULL;
|
|
struct super_block *sb = sget(type, NULL, set_anon_super, flags, NULL);
|
|
bool dir_created = false;
|
|
int error = 0;
|
|
|
|
if (IS_ERR(sb))
|
|
return ERR_CAST(sb);
|
|
|
|
sb->s_op = &incfs_super_ops;
|
|
sb->s_d_op = &incfs_dentry_ops;
|
|
sb->s_flags |= S_NOATIME;
|
|
sb->s_magic = INCFS_MAGIC_NUMBER;
|
|
sb->s_time_gran = 1;
|
|
sb->s_blocksize = INCFS_DATA_FILE_BLOCK_SIZE;
|
|
sb->s_blocksize_bits = blksize_bits(sb->s_blocksize);
|
|
sb->s_xattr = incfs_xattr_ops;
|
|
|
|
BUILD_BUG_ON(PAGE_SIZE != INCFS_DATA_FILE_BLOCK_SIZE);
|
|
|
|
if (!dev_name) {
|
|
pr_err("incfs: Backing dir is not set, filesystem can't be mounted.\n");
|
|
error = -ENOENT;
|
|
goto err_deactivate;
|
|
}
|
|
|
|
error = parse_options(&options, (char *)data);
|
|
if (error != 0) {
|
|
pr_err("incfs: Options parsing error. %d\n", error);
|
|
goto err_deactivate;
|
|
}
|
|
|
|
sb->s_bdi->ra_pages = options.readahead_pages;
|
|
if (!dev_name) {
|
|
pr_err("incfs: Backing dir is not set, filesystem can't be mounted.\n");
|
|
error = -ENOENT;
|
|
goto err_free_opts;
|
|
}
|
|
|
|
error = kern_path(dev_name, LOOKUP_FOLLOW | LOOKUP_DIRECTORY,
|
|
&backing_dir_path);
|
|
if (error || backing_dir_path.dentry == NULL ||
|
|
!d_really_is_positive(backing_dir_path.dentry)) {
|
|
pr_err("incfs: Error accessing: %s.\n",
|
|
dev_name);
|
|
goto err_free_opts;
|
|
}
|
|
src_fs_sb = backing_dir_path.dentry->d_sb;
|
|
sb->s_maxbytes = src_fs_sb->s_maxbytes;
|
|
sb->s_stack_depth = src_fs_sb->s_stack_depth + 1;
|
|
|
|
if (sb->s_stack_depth > FILESYSTEM_MAX_STACK_DEPTH) {
|
|
error = -EINVAL;
|
|
goto err_put_path;
|
|
}
|
|
|
|
mi = incfs_alloc_mount_info(sb, &options, &backing_dir_path);
|
|
if (IS_ERR_OR_NULL(mi)) {
|
|
error = PTR_ERR(mi);
|
|
pr_err("incfs: Error allocating mount info. %d\n", error);
|
|
goto err_put_path;
|
|
}
|
|
|
|
sb->s_fs_info = mi;
|
|
mi->mi_backing_dir_path = backing_dir_path;
|
|
index_dir = open_or_create_special_dir(backing_dir_path.dentry,
|
|
INCFS_INDEX_NAME, &dir_created);
|
|
if (IS_ERR_OR_NULL(index_dir)) {
|
|
error = PTR_ERR(index_dir);
|
|
pr_err("incfs: Can't find or create .index dir in %s\n",
|
|
dev_name);
|
|
/* No need to null index_dir since we don't put it */
|
|
goto err_put_path;
|
|
}
|
|
|
|
mi->mi_index_dir = index_dir;
|
|
mi->mi_index_free = dir_created;
|
|
|
|
incomplete_dir = open_or_create_special_dir(backing_dir_path.dentry,
|
|
INCFS_INCOMPLETE_NAME,
|
|
&dir_created);
|
|
if (IS_ERR_OR_NULL(incomplete_dir)) {
|
|
error = PTR_ERR(incomplete_dir);
|
|
pr_err("incfs: Can't find or create .incomplete dir in %s\n",
|
|
dev_name);
|
|
/* No need to null incomplete_dir since we don't put it */
|
|
goto err_put_path;
|
|
}
|
|
mi->mi_incomplete_dir = incomplete_dir;
|
|
mi->mi_incomplete_free = dir_created;
|
|
|
|
root_inode = fetch_regular_inode(sb, backing_dir_path.dentry);
|
|
if (IS_ERR(root_inode)) {
|
|
error = PTR_ERR(root_inode);
|
|
goto err_put_path;
|
|
}
|
|
|
|
sb->s_root = d_make_root(root_inode);
|
|
if (!sb->s_root) {
|
|
error = -ENOMEM;
|
|
goto err_put_path;
|
|
}
|
|
error = incfs_init_dentry(sb->s_root, &backing_dir_path);
|
|
if (error)
|
|
goto err_put_path;
|
|
|
|
path_put(&backing_dir_path);
|
|
sb->s_flags |= SB_ACTIVE;
|
|
|
|
pr_debug("incfs: mount\n");
|
|
return dget(sb->s_root);
|
|
|
|
err_put_path:
|
|
path_put(&backing_dir_path);
|
|
err_free_opts:
|
|
free_options(&options);
|
|
err_deactivate:
|
|
deactivate_locked_super(sb);
|
|
pr_err("incfs: mount failed %d\n", error);
|
|
return ERR_PTR(error);
|
|
}
|
|
|
|
static int incfs_remount_fs(struct super_block *sb, int *flags, char *data)
|
|
{
|
|
struct mount_options options;
|
|
struct mount_info *mi = get_mount_info(sb);
|
|
int err = 0;
|
|
|
|
sync_filesystem(sb);
|
|
err = parse_options(&options, (char *)data);
|
|
if (err)
|
|
return err;
|
|
|
|
if (options.report_uid != mi->mi_options.report_uid) {
|
|
pr_err("incfs: Can't change report_uid mount option on remount\n");
|
|
err = -EOPNOTSUPP;
|
|
goto out;
|
|
}
|
|
|
|
err = incfs_realloc_mount_info(mi, &options);
|
|
if (err)
|
|
goto out;
|
|
|
|
pr_debug("incfs: remount\n");
|
|
|
|
out:
|
|
free_options(&options);
|
|
return err;
|
|
}
|
|
|
|
void incfs_kill_sb(struct super_block *sb)
|
|
{
|
|
struct mount_info *mi = sb->s_fs_info;
|
|
struct inode *dinode = NULL;
|
|
|
|
pr_debug("incfs: unmount\n");
|
|
|
|
if (mi) {
|
|
if (mi->mi_backing_dir_path.dentry)
|
|
dinode = d_inode(mi->mi_backing_dir_path.dentry);
|
|
|
|
if (dinode) {
|
|
if (mi->mi_index_dir && mi->mi_index_free)
|
|
vfs_rmdir(dinode, mi->mi_index_dir);
|
|
|
|
if (mi->mi_incomplete_dir && mi->mi_incomplete_free)
|
|
vfs_rmdir(dinode, mi->mi_incomplete_dir);
|
|
}
|
|
|
|
incfs_free_mount_info(mi);
|
|
sb->s_fs_info = NULL;
|
|
}
|
|
kill_anon_super(sb);
|
|
}
|
|
|
|
static int show_options(struct seq_file *m, struct dentry *root)
|
|
{
|
|
struct mount_info *mi = get_mount_info(root->d_sb);
|
|
|
|
seq_printf(m, ",read_timeout_ms=%u", mi->mi_options.read_timeout_ms);
|
|
seq_printf(m, ",readahead=%u", mi->mi_options.readahead_pages);
|
|
if (mi->mi_options.read_log_pages != 0) {
|
|
seq_printf(m, ",rlog_pages=%u", mi->mi_options.read_log_pages);
|
|
seq_printf(m, ",rlog_wakeup_cnt=%u",
|
|
mi->mi_options.read_log_wakeup_count);
|
|
}
|
|
if (mi->mi_options.report_uid)
|
|
seq_puts(m, ",report_uid");
|
|
|
|
if (mi->mi_sysfs_node)
|
|
seq_printf(m, ",sysfs_name=%s",
|
|
kobject_name(&mi->mi_sysfs_node->isn_sysfs_node));
|
|
return 0;
|
|
}
|