| // SPDX-License-Identifier: MIT |
| /* |
| * VirtualBox Guest Shared Folders support: Directory inode and file operations |
| * |
| * Copyright (C) 2006-2018 Oracle Corporation |
| */ |
| |
| #include <linux/namei.h> |
| #include <linux/vbox_utils.h> |
| #include "vfsmod.h" |
| |
| static int vboxsf_dir_open(struct inode *inode, struct file *file) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(inode->i_sb); |
| struct shfl_createparms params = {}; |
| struct vboxsf_dir_info *sf_d; |
| int err; |
| |
| sf_d = vboxsf_dir_info_alloc(); |
| if (!sf_d) |
| return -ENOMEM; |
| |
| params.handle = SHFL_HANDLE_NIL; |
| params.create_flags = SHFL_CF_DIRECTORY | SHFL_CF_ACT_OPEN_IF_EXISTS | |
| SHFL_CF_ACT_FAIL_IF_NEW | SHFL_CF_ACCESS_READ; |
| |
| err = vboxsf_create_at_dentry(file_dentry(file), ¶ms); |
| if (err) |
| goto err_free_dir_info; |
| |
| if (params.result != SHFL_FILE_EXISTS) { |
| err = -ENOENT; |
| goto err_close; |
| } |
| |
| err = vboxsf_dir_read_all(sbi, sf_d, params.handle); |
| if (err) |
| goto err_close; |
| |
| vboxsf_close(sbi->root, params.handle); |
| file->private_data = sf_d; |
| return 0; |
| |
| err_close: |
| vboxsf_close(sbi->root, params.handle); |
| err_free_dir_info: |
| vboxsf_dir_info_free(sf_d); |
| return err; |
| } |
| |
| static int vboxsf_dir_release(struct inode *inode, struct file *file) |
| { |
| if (file->private_data) |
| vboxsf_dir_info_free(file->private_data); |
| |
| return 0; |
| } |
| |
| static unsigned int vboxsf_get_d_type(u32 mode) |
| { |
| unsigned int d_type; |
| |
| switch (mode & SHFL_TYPE_MASK) { |
| case SHFL_TYPE_FIFO: |
| d_type = DT_FIFO; |
| break; |
| case SHFL_TYPE_DEV_CHAR: |
| d_type = DT_CHR; |
| break; |
| case SHFL_TYPE_DIRECTORY: |
| d_type = DT_DIR; |
| break; |
| case SHFL_TYPE_DEV_BLOCK: |
| d_type = DT_BLK; |
| break; |
| case SHFL_TYPE_FILE: |
| d_type = DT_REG; |
| break; |
| case SHFL_TYPE_SYMLINK: |
| d_type = DT_LNK; |
| break; |
| case SHFL_TYPE_SOCKET: |
| d_type = DT_SOCK; |
| break; |
| case SHFL_TYPE_WHITEOUT: |
| d_type = DT_WHT; |
| break; |
| default: |
| d_type = DT_UNKNOWN; |
| break; |
| } |
| return d_type; |
| } |
| |
| static bool vboxsf_dir_emit(struct file *dir, struct dir_context *ctx) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(file_inode(dir)->i_sb); |
| struct vboxsf_dir_info *sf_d = dir->private_data; |
| struct shfl_dirinfo *info; |
| struct vboxsf_dir_buf *b; |
| unsigned int d_type; |
| loff_t i, cur = 0; |
| ino_t fake_ino; |
| void *end; |
| int err; |
| |
| list_for_each_entry(b, &sf_d->info_list, head) { |
| try_next_entry: |
| if (ctx->pos >= cur + b->entries) { |
| cur += b->entries; |
| continue; |
| } |
| |
| /* |
| * Note the vboxsf_dir_info objects we are iterating over here |
| * are variable sized, so the info pointer may end up being |
| * unaligned. This is how we get the data from the host. |
| * Since vboxsf is only supported on x86 machines this is not |
| * a problem. |
| */ |
| for (i = 0, info = b->buf; i < ctx->pos - cur; i++) { |
| end = &info->name.string.utf8[info->name.size]; |
| /* Only happens if the host gives us corrupt data */ |
| if (WARN_ON(end > (b->buf + b->used))) |
| return false; |
| info = end; |
| } |
| |
| end = &info->name.string.utf8[info->name.size]; |
| if (WARN_ON(end > (b->buf + b->used))) |
| return false; |
| |
| /* Info now points to the right entry, emit it. */ |
| d_type = vboxsf_get_d_type(info->info.attr.mode); |
| |
| /* |
| * On 32-bit systems pos is 64-bit signed, while ino is 32-bit |
| * unsigned so fake_ino may overflow, check for this. |
| */ |
| if ((ino_t)(ctx->pos + 1) != (u64)(ctx->pos + 1)) { |
| vbg_err("vboxsf: fake ino overflow, truncating dir\n"); |
| return false; |
| } |
| fake_ino = ctx->pos + 1; |
| |
| if (sbi->nls) { |
| char d_name[NAME_MAX]; |
| |
| err = vboxsf_nlscpy(sbi, d_name, NAME_MAX, |
| info->name.string.utf8, |
| info->name.length); |
| if (err) { |
| /* skip erroneous entry and proceed */ |
| ctx->pos += 1; |
| goto try_next_entry; |
| } |
| |
| return dir_emit(ctx, d_name, strlen(d_name), |
| fake_ino, d_type); |
| } |
| |
| return dir_emit(ctx, info->name.string.utf8, info->name.length, |
| fake_ino, d_type); |
| } |
| |
| return false; |
| } |
| |
| static int vboxsf_dir_iterate(struct file *dir, struct dir_context *ctx) |
| { |
| bool emitted; |
| |
| do { |
| emitted = vboxsf_dir_emit(dir, ctx); |
| if (emitted) |
| ctx->pos += 1; |
| } while (emitted); |
| |
| return 0; |
| } |
| |
| const struct file_operations vboxsf_dir_fops = { |
| .open = vboxsf_dir_open, |
| .iterate = vboxsf_dir_iterate, |
| .release = vboxsf_dir_release, |
| .read = generic_read_dir, |
| .llseek = generic_file_llseek, |
| }; |
| |
| /* |
| * This is called during name resolution/lookup to check if the @dentry in |
| * the cache is still valid. the job is handled by vboxsf_inode_revalidate. |
| */ |
| static int vboxsf_dentry_revalidate(struct dentry *dentry, unsigned int flags) |
| { |
| if (flags & LOOKUP_RCU) |
| return -ECHILD; |
| |
| if (d_really_is_positive(dentry)) |
| return vboxsf_inode_revalidate(dentry) == 0; |
| else |
| return vboxsf_stat_dentry(dentry, NULL) == -ENOENT; |
| } |
| |
| const struct dentry_operations vboxsf_dentry_ops = { |
| .d_revalidate = vboxsf_dentry_revalidate |
| }; |
| |
| /* iops */ |
| |
| static struct dentry *vboxsf_dir_lookup(struct inode *parent, |
| struct dentry *dentry, |
| unsigned int flags) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(parent->i_sb); |
| struct shfl_fsobjinfo fsinfo; |
| struct inode *inode; |
| int err; |
| |
| dentry->d_time = jiffies; |
| |
| err = vboxsf_stat_dentry(dentry, &fsinfo); |
| if (err) { |
| inode = (err == -ENOENT) ? NULL : ERR_PTR(err); |
| } else { |
| inode = vboxsf_new_inode(parent->i_sb); |
| if (!IS_ERR(inode)) |
| vboxsf_init_inode(sbi, inode, &fsinfo); |
| } |
| |
| return d_splice_alias(inode, dentry); |
| } |
| |
| static int vboxsf_dir_instantiate(struct inode *parent, struct dentry *dentry, |
| struct shfl_fsobjinfo *info) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(parent->i_sb); |
| struct vboxsf_inode *sf_i; |
| struct inode *inode; |
| |
| inode = vboxsf_new_inode(parent->i_sb); |
| if (IS_ERR(inode)) |
| return PTR_ERR(inode); |
| |
| sf_i = VBOXSF_I(inode); |
| /* The host may have given us different attr then requested */ |
| sf_i->force_restat = 1; |
| vboxsf_init_inode(sbi, inode, info); |
| |
| d_instantiate(dentry, inode); |
| |
| return 0; |
| } |
| |
| static int vboxsf_dir_create(struct inode *parent, struct dentry *dentry, |
| umode_t mode, bool is_dir, bool excl, u64 *handle_ret) |
| { |
| struct vboxsf_inode *sf_parent_i = VBOXSF_I(parent); |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(parent->i_sb); |
| struct shfl_createparms params = {}; |
| int err; |
| |
| params.handle = SHFL_HANDLE_NIL; |
| params.create_flags = SHFL_CF_ACT_CREATE_IF_NEW | SHFL_CF_ACCESS_READWRITE; |
| if (is_dir) |
| params.create_flags |= SHFL_CF_DIRECTORY; |
| if (excl) |
| params.create_flags |= SHFL_CF_ACT_FAIL_IF_EXISTS; |
| |
| params.info.attr.mode = (mode & 0777) | |
| (is_dir ? SHFL_TYPE_DIRECTORY : SHFL_TYPE_FILE); |
| params.info.attr.additional = SHFLFSOBJATTRADD_NOTHING; |
| |
| err = vboxsf_create_at_dentry(dentry, ¶ms); |
| if (err) |
| return err; |
| |
| if (params.result != SHFL_FILE_CREATED) |
| return -EPERM; |
| |
| err = vboxsf_dir_instantiate(parent, dentry, ¶ms.info); |
| if (err) |
| goto out; |
| |
| /* parent directory access/change time changed */ |
| sf_parent_i->force_restat = 1; |
| |
| out: |
| if (err == 0 && handle_ret) |
| *handle_ret = params.handle; |
| else |
| vboxsf_close(sbi->root, params.handle); |
| |
| return err; |
| } |
| |
| static int vboxsf_dir_mkfile(struct inode *parent, struct dentry *dentry, |
| umode_t mode, bool excl) |
| { |
| return vboxsf_dir_create(parent, dentry, mode, false, excl, NULL); |
| } |
| |
| static int vboxsf_dir_mkdir(struct inode *parent, struct dentry *dentry, |
| umode_t mode) |
| { |
| return vboxsf_dir_create(parent, dentry, mode, true, true, NULL); |
| } |
| |
| static int vboxsf_dir_atomic_open(struct inode *parent, struct dentry *dentry, |
| struct file *file, unsigned int flags, umode_t mode) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(parent->i_sb); |
| struct vboxsf_handle *sf_handle; |
| struct dentry *res = NULL; |
| u64 handle; |
| int err; |
| |
| if (d_in_lookup(dentry)) { |
| res = vboxsf_dir_lookup(parent, dentry, 0); |
| if (IS_ERR(res)) |
| return PTR_ERR(res); |
| |
| if (res) |
| dentry = res; |
| } |
| |
| /* Only creates */ |
| if (!(flags & O_CREAT) || d_really_is_positive(dentry)) |
| return finish_no_open(file, res); |
| |
| err = vboxsf_dir_create(parent, dentry, mode, false, flags & O_EXCL, &handle); |
| if (err) |
| goto out; |
| |
| sf_handle = vboxsf_create_sf_handle(d_inode(dentry), handle, SHFL_CF_ACCESS_READWRITE); |
| if (IS_ERR(sf_handle)) { |
| vboxsf_close(sbi->root, handle); |
| err = PTR_ERR(sf_handle); |
| goto out; |
| } |
| |
| err = finish_open(file, dentry, generic_file_open); |
| if (err) { |
| /* This also closes the handle passed to vboxsf_create_sf_handle() */ |
| vboxsf_release_sf_handle(d_inode(dentry), sf_handle); |
| goto out; |
| } |
| |
| file->private_data = sf_handle; |
| file->f_mode |= FMODE_CREATED; |
| out: |
| dput(res); |
| return err; |
| } |
| |
| static int vboxsf_dir_unlink(struct inode *parent, struct dentry *dentry) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(parent->i_sb); |
| struct vboxsf_inode *sf_parent_i = VBOXSF_I(parent); |
| struct inode *inode = d_inode(dentry); |
| struct shfl_string *path; |
| u32 flags; |
| int err; |
| |
| if (S_ISDIR(inode->i_mode)) |
| flags = SHFL_REMOVE_DIR; |
| else |
| flags = SHFL_REMOVE_FILE; |
| |
| if (S_ISLNK(inode->i_mode)) |
| flags |= SHFL_REMOVE_SYMLINK; |
| |
| path = vboxsf_path_from_dentry(sbi, dentry); |
| if (IS_ERR(path)) |
| return PTR_ERR(path); |
| |
| err = vboxsf_remove(sbi->root, path, flags); |
| __putname(path); |
| if (err) |
| return err; |
| |
| /* parent directory access/change time changed */ |
| sf_parent_i->force_restat = 1; |
| |
| return 0; |
| } |
| |
| static int vboxsf_dir_rename(struct inode *old_parent, |
| struct dentry *old_dentry, |
| struct inode *new_parent, |
| struct dentry *new_dentry, |
| unsigned int flags) |
| { |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(old_parent->i_sb); |
| struct vboxsf_inode *sf_old_parent_i = VBOXSF_I(old_parent); |
| struct vboxsf_inode *sf_new_parent_i = VBOXSF_I(new_parent); |
| u32 shfl_flags = SHFL_RENAME_FILE | SHFL_RENAME_REPLACE_IF_EXISTS; |
| struct shfl_string *old_path, *new_path; |
| int err; |
| |
| if (flags) |
| return -EINVAL; |
| |
| old_path = vboxsf_path_from_dentry(sbi, old_dentry); |
| if (IS_ERR(old_path)) |
| return PTR_ERR(old_path); |
| |
| new_path = vboxsf_path_from_dentry(sbi, new_dentry); |
| if (IS_ERR(new_path)) { |
| err = PTR_ERR(new_path); |
| goto err_put_old_path; |
| } |
| |
| if (d_inode(old_dentry)->i_mode & S_IFDIR) |
| shfl_flags = 0; |
| |
| err = vboxsf_rename(sbi->root, old_path, new_path, shfl_flags); |
| if (err == 0) { |
| /* parent directories access/change time changed */ |
| sf_new_parent_i->force_restat = 1; |
| sf_old_parent_i->force_restat = 1; |
| } |
| |
| __putname(new_path); |
| err_put_old_path: |
| __putname(old_path); |
| return err; |
| } |
| |
| static int vboxsf_dir_symlink(struct inode *parent, struct dentry *dentry, |
| const char *symname) |
| { |
| struct vboxsf_inode *sf_parent_i = VBOXSF_I(parent); |
| struct vboxsf_sbi *sbi = VBOXSF_SBI(parent->i_sb); |
| int symname_size = strlen(symname) + 1; |
| struct shfl_string *path, *ssymname; |
| struct shfl_fsobjinfo info; |
| int err; |
| |
| path = vboxsf_path_from_dentry(sbi, dentry); |
| if (IS_ERR(path)) |
| return PTR_ERR(path); |
| |
| ssymname = kmalloc(SHFLSTRING_HEADER_SIZE + symname_size, GFP_KERNEL); |
| if (!ssymname) { |
| __putname(path); |
| return -ENOMEM; |
| } |
| ssymname->length = symname_size - 1; |
| ssymname->size = symname_size; |
| memcpy(ssymname->string.utf8, symname, symname_size); |
| |
| err = vboxsf_symlink(sbi->root, path, ssymname, &info); |
| kfree(ssymname); |
| __putname(path); |
| if (err) { |
| /* -EROFS means symlinks are note support -> -EPERM */ |
| return (err == -EROFS) ? -EPERM : err; |
| } |
| |
| err = vboxsf_dir_instantiate(parent, dentry, &info); |
| if (err) |
| return err; |
| |
| /* parent directory access/change time changed */ |
| sf_parent_i->force_restat = 1; |
| return 0; |
| } |
| |
| const struct inode_operations vboxsf_dir_iops = { |
| .lookup = vboxsf_dir_lookup, |
| .create = vboxsf_dir_mkfile, |
| .mkdir = vboxsf_dir_mkdir, |
| .atomic_open = vboxsf_dir_atomic_open, |
| .rmdir = vboxsf_dir_unlink, |
| .unlink = vboxsf_dir_unlink, |
| .rename = vboxsf_dir_rename, |
| .symlink = vboxsf_dir_symlink, |
| .getattr = vboxsf_getattr, |
| .setattr = vboxsf_setattr, |
| }; |